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

feat(cfn-include): add 'loadNestedStack()' method #10292

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 10 additions & 1 deletion packages/@aws-cdk/cloudformation-include/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ and the nested stack in your CDK application as follows:
```typescript
const parentTemplate = new inc.CfnInclude(this, 'ParentStack', {
templateFile: 'path/to/my-parent-template.json',
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: 'path/to/my-nested-template.json',
},
Expand Down Expand Up @@ -299,6 +299,15 @@ role.addToPolicy(new iam.PolicyStatement({
}));
```

You can also include the nested stack after the `CfnInclude` object was created,
instead of doing it on construction:

```ts
const includedChildStack = parentTemplate.loadNestedStack('ChildTemplate', {
templateFile: 'path/to/my-nested-template.json',
});
```

## Vending CloudFormation templates as Constructs

In many cases, there are existing CloudFormation templates that are not entire applications,
Expand Down
50 changes: 43 additions & 7 deletions packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ export interface CfnIncludeProps {
* Specifies the template files that define nested stacks that should be included.
*
* If your template specifies a stack that isn't included here, it won't be created as a NestedStack
* resource, and it won't be accessible from {@link CfnInclude.getNestedStack}.
* resource, and it won't be accessible from the {@link CfnInclude.getNestedStack} method
* (but will still be accessible from the {@link CfnInclude.getResource} method).
*
* If you include a stack here with an ID that isn't in the template,
* or is in the template but is not a nested stack,
* template creation will fail and an error will be thrown.
*
* @default - no nested stacks will be included
*/
readonly nestedStacks?: { [stackName: string]: CfnIncludeProps };
readonly loadNestedStacks?: { [stackName: string]: CfnIncludeProps };

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

this.nestedStacksToInclude = props.nestedStacks || {};
this.nestedStacksToInclude = props.loadNestedStacks || {};
// 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 || {})) {
for (const nestedStackId of Object.keys(props.loadNestedStacks || {})) {
if (!(nestedStackId in this.resources)) {
throw new Error(`Nested Stack with logical ID '${nestedStackId}' was not found in the template`);
}
Expand Down Expand Up @@ -291,8 +292,9 @@ export class CfnInclude extends core.CfnElement {

/**
* Returns the NestedStack with name logicalId.
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
* For a nested stack to be returned by this method, it must be specified in the {@link CfnIncludeProps.nestedStacks}
* property.
* For a nested stack to be returned by this method,
* it must be specified either in the {@link CfnIncludeProps.loadNestedStacks} property,
* or through the {@link loadNestedStack} method.
*
* @param logicalId the ID of the stack to retrieve, as it appears in the template
*/
Expand All @@ -303,12 +305,46 @@ export class CfnInclude extends core.CfnElement {
} else if (this.template.Resources[logicalId].Type !== 'AWS::CloudFormation::Stack') {
throw new Error(`Resource with logical ID '${logicalId}' is not a CloudFormation Stack`);
} else {
throw new Error(`Nested Stack '${logicalId}' was not included in the nestedStacks property when including the parent template`);
throw new Error(`Nested Stack '${logicalId}' was not included in the loadNestedStacks property when including the parent template`);
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
}
}
return this.nestedStacks[logicalId];
}

/**
* Includes a template for a child stack inside of this parent template.
* A child with this logical ID must exist in the template,
* and be of type AWS::CloudFormation::Stack.
* This is equivalent to specifying the value in the {@link CfnIncludeProps.loadNestedStacks}
* property on object construction.
*
* @param logicalId the ID of the stack to retrieve, as it appears in the template
* @param nestedStackProps the properties of the included child Stack
* @returns the same {@link IncludedNestedStack} object that {@link getNestedStack} returns for this logical ID
*/
public loadNestedStack(logicalId: string, nestedStackProps: CfnIncludeProps): IncludedNestedStack {
if (logicalId in this.nestedStacks) {
throw new Error(`Nested Stack '${logicalId}' was already included in its parent template`);
}
const cfnStack = this.resources[logicalId];
if (!cfnStack) {
throw new Error(`Nested Stack with logical ID '${logicalId}' was not found in the template`);
}
if (cfnStack instanceof core.CfnStack) {
// delete the old CfnStack child - one will be created by the NestedStack object
this.node.tryRemoveChild(logicalId);
// remove the previously created CfnStack resource from the resources map
delete this.resources[logicalId];
// createNestedStack() (called by getOrCreateResource()) expects this to be filled
this.nestedStacksToInclude[logicalId] = nestedStackProps;

this.getOrCreateResource(logicalId);
return this.nestedStacks[logicalId];
} else {
throw new Error(`Nested Stack with logical ID '${logicalId}' is not an AWS::CloudFormation::Stack resource`);
}
}

/** @internal */
public _toCloudFormation(): object {
const ret: { [section: string]: any } = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/cloudformation-include/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
},
"awslint": {
"exclude": [
"props-no-cfn-types:@aws-cdk/cloudformation-include.CfnIncludeProps.nestedStacks"
"props-no-cfn-types:@aws-cdk/cloudformation-include.CfnIncludeProps.loadNestedStacks"
]
},
"stability": "experimental",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const stack = new core.Stack(app, 'ParentStack');

new inc.CfnInclude(stack, 'ParentStack', {
templateFile: 'test-templates/nested/parent-one-child.json',
nestedStacks: {
loadNestedStacks: {
ChildStack: {
templateFile: 'test-templates/nested/grandchild-import-stack.json',
},
Expand Down
70 changes: 42 additions & 28 deletions packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('CDK Include for nested stacks', () => {
test('can ingest a template with one child', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-one-child.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -35,7 +35,7 @@ describe('CDK Include for nested stacks', () => {
test('can ingest a template with two children', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -59,10 +59,10 @@ describe('CDK Include for nested stacks', () => {
test('can ingest a template with one child and one grandchild', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
nestedStacks: {
loadNestedStacks: {
'GrandChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -86,7 +86,7 @@ describe('CDK Include for nested stacks', () => {
expect(() => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
nestedStacks: {
loadNestedStacks: {
'FakeStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -99,7 +99,7 @@ describe('CDK Include for nested stacks', () => {
expect(() => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('child-import-stack.json'),
nestedStacks: {
loadNestedStacks: {
'BucketImport': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -112,7 +112,7 @@ describe('CDK Include for nested stacks', () => {
expect(() => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-creation-policy.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -125,7 +125,7 @@ describe('CDK Include for nested stacks', () => {
expect(() => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-update-policy.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -138,7 +138,7 @@ describe('CDK Include for nested stacks', () => {
expect(() => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-invalid-condition.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -151,7 +151,7 @@ describe('CDK Include for nested stacks', () => {
expect(() => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-bad-depends-on.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -160,11 +160,11 @@ describe('CDK Include for nested stacks', () => {
}).toThrow(/Resource 'ChildStack' depends on 'AFakeResource' that doesn't exist/);
});

test('throws an exception when an ID was passed in nestedStacks that is a resource type not in the CloudFormation schema', () => {
test('throws an exception when an ID was passed in loadNestedStacks that is a resource type not in the CloudFormation schema', () => {
expect(() => {
new inc.CfnInclude(stack, 'Template', {
templateFile: testTemplateFilePath('custom-resource.json'),
nestedStacks: {
loadNestedStacks: {
'CustomResource': {
templateFile: testTemplateFilePath('whatever.json'),
},
Expand All @@ -176,7 +176,7 @@ describe('CDK Include for nested stacks', () => {
test('can modify resources in nested stacks', () => {
const parent = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('child-import-stack.json'),
nestedStacks: {
loadNestedStacks: {
'GrandChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -194,7 +194,7 @@ describe('CDK Include for nested stacks', () => {
test('can use a condition', () => {
const parent = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-valid-condition.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand All @@ -209,7 +209,7 @@ describe('CDK Include for nested stacks', () => {
test('asset parameters generated in parent and child are identical', () => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-one-child.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand Down Expand Up @@ -279,7 +279,7 @@ describe('CDK Include for nested stacks', () => {
});
});

test('templates with nested stacks that were not provided in the nestedStacks property are left unmodified', () => {
test('templates with nested stacks that were not provided in the loadNestedStacks property are left unmodified', () => {
new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
});
Expand All @@ -290,7 +290,7 @@ describe('CDK Include for nested stacks', () => {
test('getNestedStack() throws an exception when getting a resource that does not exist in the template', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -305,7 +305,7 @@ describe('CDK Include for nested stacks', () => {
test('getNestedStack() throws an exception when getting a resource that exists in the template, but is not a Stack', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -322,7 +322,7 @@ describe('CDK Include for nested stacks', () => {
test('getNestedStack() throws an exception when getting a resource that exists in the template, but was not specified in the props', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-children.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -331,13 +331,13 @@ describe('CDK Include for nested stacks', () => {

expect(() => {
parentTemplate.getNestedStack('AnotherChildStack');
}).toThrow(/Nested Stack 'AnotherChildStack' was not included in the nestedStacks property when including the parent template/);
}).toThrow(/Nested Stack 'AnotherChildStack' was not included in the loadNestedStacks property when including the parent template/);
});

test('correctly handles renaming of references across nested stacks', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('cross-stack-refs.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -360,7 +360,7 @@ describe('CDK Include for nested stacks', () => {
});
});

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

test('returns the CfnStack object from getResource() for a nested stack that was in the nestedStacks property', () => {
test('returns the CfnStack object from getResource() for a nested stack that was in the loadNestedStacks property', () => {
const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-one-child.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand All @@ -388,7 +388,7 @@ describe('CDK Include for nested stacks', () => {
test("handles Metadata, DeletionPolicy, and UpdateReplacePolicy attributes of the nested stack's resource", () => {
const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-with-attributes.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-import-stack.json'),
},
Expand Down Expand Up @@ -424,6 +424,20 @@ describe('CDK Include for nested stacks', () => {
});
});

test('can lazily include a single child nested stack', () => {
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-one-child.json'),
});
const includedChild = parentTemplate.loadNestedStack('ChildStack', {
templateFile: testTemplateFilePath('child-no-bucket.json'),
});

expect(includedChild.stack).toMatchTemplate(
loadTestFileToJsObject('child-no-bucket.json'),
);
expect(includedChild.includedTemplate.getResource('GrandChildStack')).toBeDefined();
});

describe('for a parent stack with children and grandchildren', () => {
let assetStack: core.Stack;
let parentTemplate: inc.CfnInclude;
Expand All @@ -442,10 +456,10 @@ describe('CDK Include for nested stacks', () => {
assetStack = new core.Stack();
parentTemplate = new inc.CfnInclude(assetStack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-one-child.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-no-bucket.json'),
nestedStacks: {
loadNestedStacks: {
'GrandChildStack': {
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
},
Expand Down Expand Up @@ -621,7 +635,7 @@ describe('CDK Include for nested stacks', () => {
parentStack = new core.Stack();
const parentTemplate = new inc.CfnInclude(parentStack, 'ParentStack', {
templateFile: testTemplateFilePath('parent-two-parameters.json'),
nestedStacks: {
loadNestedStacks: {
'ChildStack': {
templateFile: testTemplateFilePath('child-two-parameters.json'),
parameters: {
Expand Down