diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 52da55a585b02..1b1468e490bef 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -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', }, @@ -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, diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index 1a74dd14f5108..c19361247bf09 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -32,7 +32,8 @@ 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, @@ -40,7 +41,7 @@ export interface CfnIncludeProps { * * @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. @@ -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`); } @@ -290,9 +291,10 @@ 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} - * property. + * Returns a loaded NestedStack with name logicalId. + * 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 */ @@ -303,12 +305,47 @@ 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 parent template. ` + + 'To retrieve an included nested stack, it must be specified either in the `loadNestedStacks` property, or through the `loadNestedStack` method'); } } 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 } = {}; diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 6dfa741afb9b7..a365a37176c39 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -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", diff --git a/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts b/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts index 5ccd876c328db..11d2391e06582 100644 --- a/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts +++ b/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts @@ -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', }, diff --git a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts index 69ce97dcca07d..bf729dcedd38b 100644 --- a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }, @@ -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'), }); @@ -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'), }, @@ -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'), }, @@ -319,10 +319,10 @@ describe('CDK Include for nested stacks', () => { }).toThrow(/Resource with logical ID 'BucketImport' is not a CloudFormation Stack/); }); - test('getNestedStack() throws an exception when getting a resource that exists in the template, but was not specified in the props', () => { + test('getNestedStack() throws an exception when getting a nested stack 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'), }, @@ -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 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'), }, @@ -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'), }); @@ -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'), }, @@ -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'), }, @@ -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; @@ -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'), }, @@ -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: {