From bf12456671e171eab16690fc8b54fae6841cf711 Mon Sep 17 00:00:00 2001 From: comcalvi <66279577+comcalvi@users.noreply.github.com> Date: Thu, 16 Jul 2020 16:30:30 -0400 Subject: [PATCH] feat(cfn-include): add support for nested stacks (#8980) Fixes #8978 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/cloudformation-include/README.md | 86 +++ .../cloudformation-include/lib/cfn-include.ts | 126 +++- .../cloudformation-include/package.json | 9 +- .../test/integ.nested-stacks.expected.json | 72 +++ .../test/integ.nested-stacks.ts | 17 + .../test/invalid-templates.test.ts | 2 +- .../test/nested-stacks.test.ts | 588 ++++++++++++++++++ .../nested/child-import-stack.expected.json | 78 +++ .../nested/child-import-stack.json | 27 + .../nested/child-no-bucket.json | 19 + .../nested/cross-stack-refs.json | 22 + .../nested/grandchild-import-stack.json | 26 + .../nested/only-nested-stack.expected.json | 69 ++ .../nested/only-nested-stack.json | 10 + .../nested/parent-bad-depends-on.json | 13 + .../nested/parent-creation-policy.json | 15 + .../nested/parent-invalid-condition.json | 14 + .../nested/parent-one-child.json | 13 + .../nested/parent-two-children.json | 22 + .../nested/parent-update-policy.json | 15 + .../nested/parent-valid-condition.json | 24 + .../nested/parent-with-attributes.json | 26 + .../test/valid-templates.test.ts | 2 +- .../test/yaml-templates.test.ts | 2 +- 24 files changed, 1292 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.expected.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts create mode 100644 packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.expected.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-no-bucket.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/cross-stack-refs.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/grandchild-import-stack.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.expected.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-bad-depends-on.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-creation-policy.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-invalid-condition.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-one-child.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-two-children.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-update-policy.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-valid-condition.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-with-attributes.json diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 01be06067dde4..e8ea292934958 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -142,6 +142,92 @@ and any changes you make to it will be reflected in the resulting template: output.value = cfnBucket.attrArn; ``` +## Nested Stacks + +This module also support templates that use [nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html). + +For example, if you have the following parent template: + +```json +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://my-s3-template-source.s3.amazonaws.com/child-import-stack.json", + "Parameters": { + "MyBucketParameter": "my-bucket-name" + } + } + } + } +} +``` + +where the child template pointed to by `https://my-s3-template-source.s3.amazonaws.com/child-import-stack.json` is: + +```json +{ + "Parameters": { + "MyBucketParameter": { + "Type": "String", + "Default": "default-bucket-param-name" + } + }, + "Resources": { + "BucketImport": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "MyBucketParameter" + } + } + } + } +} +``` + +You can include both the parent stack and the nested stack in your CDK Application as follows: + +```typescript +const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: 'path/to/my-parent-template.json', + nestedStacks: { + 'ChildStack': { + templateFile: 'path/to/my-nested-template.json', + }, + }, +}); +``` + +Now you can access the ChildStack nested stack and included template with: + +```typescript +const includedChildStack = parentTemplate.getNestedStack('ChildStack'); +const childStack: core.NestedStack = includedChildStack.stack; +const childStackTemplate: cfn_inc.CfnInclude = includedChildStack.includedTemplate; +``` + +Now you can reference resources from `ChildStack` and modify them like any other included template: + +```typescript +const bucket = childStackTemplate.getResource('MyBucket') as s3.CfnBucket; +bucket.bucketName = 'my-new-bucket-name'; + +const bucketReadRole = new iam.Role(childStack, 'MyRole', { + assumedBy: new iam.AccountRootPrincipal(), +}); + +bucketReadRole.addToPolicy(new iam.PolicyStatement({ + actions: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + resources: [bucket.attrArn], +})); +``` + ## Known limitations This module is still in its early, experimental stage, diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index 035631fd282d5..7be10e373ae45 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -13,6 +13,36 @@ export interface CfnIncludeProps { * Both JSON and YAML template formats are supported. */ readonly templateFile: string; + + /** + * 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}. + * + * 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. + */ + readonly nestedStacks?: { [stackName: string]: CfnIncludeProps }; +} + +/** + * The type returned from {@link CfnInclude.getNestedStack}. + * Contains both the NestedStack object and + * CfnInclude representations of the child stack. + */ +export interface IncludedNestedStack { + /** + * The NestedStack object which respresents the scope of the template. + */ + readonly stack: core.NestedStack; + + /** + * The CfnInclude that respresents the template, which can + * be used to access Resources and other template elements. + */ + readonly includedTemplate: CfnInclude; } /** @@ -25,6 +55,8 @@ export class CfnInclude extends core.CfnElement { private readonly resources: { [logicalId: string]: core.CfnResource } = {}; private readonly parameters: { [logicalId: string]: core.CfnParameter } = {}; private readonly outputs: { [logicalId: string]: core.CfnOutput } = {}; + private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {}; + private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps }; private readonly template: any; private readonly preserveLogicalIds: boolean; @@ -47,11 +79,20 @@ export class CfnInclude extends core.CfnElement { this.createCondition(conditionName); } + 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)) { + throw new Error(`Nested Stack with logical ID '${nestedStackId}' was not found in the template`); + } + } + const outputScope = new core.Construct(this, '$Ouputs'); for (const logicalId of Object.keys(this.template.Outputs || {})) { @@ -137,6 +178,24 @@ export class CfnInclude extends core.CfnElement { return ret; } + /** + * 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. + */ + public getNestedStack(logicalId: string): IncludedNestedStack { + if (!this.nestedStacks[logicalId]) { + if (!this.template.Resources[logicalId]) { + throw new Error(`Nested Stack with logical ID '${logicalId}' was not found in the template`); + } 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`); + } + } + return this.nestedStacks[logicalId]; + } + /** @internal */ public _toCloudFormation(): object { const ret: { [section: string]: any } = {}; @@ -283,7 +342,10 @@ export class CfnInclude extends core.CfnElement { const options: core.FromCloudFormationOptions = { finder, }; - const l1Instance = jsClassFromModule.fromCloudFormation(this, logicalId, resourceAttributes, options); + + const l1Instance = this.nestedStacksToInclude[logicalId] + ? this.createNestedStack(logicalId, finder) + : jsClassFromModule.fromCloudFormation(this, logicalId, resourceAttributes, options); if (this.preserveLogicalIds) { // override the logical ID to match the original template @@ -293,4 +355,66 @@ export class CfnInclude extends core.CfnElement { this.resources[logicalId] = l1Instance; return l1Instance; } + + private createNestedStack(nestedStackId: string, finder: core.ICfnFinder): core.CfnResource { + const templateResources = this.template.Resources || {}; + const nestedStackAttributes = templateResources[nestedStackId] || {}; + + if (nestedStackAttributes.Type !== 'AWS::CloudFormation::Stack') { + throw new Error(`Nested Stack with logical ID '${nestedStackId}' is not an AWS::CloudFormation::Stack resource`); + } + if (nestedStackAttributes.CreationPolicy) { + throw new Error('CreationPolicy is not supported by the AWS::CloudFormation::Stack resource'); + } + if (nestedStackAttributes.UpdatePolicy) { + throw new Error('UpdatePolicy is not supported by the AWS::CloudFormation::Stack resource'); + } + + const cfnParser = new cfn_parse.CfnParser({ + finder, + }); + const nestedStackProps = cfnParser.parseValue(nestedStackAttributes.Properties); + const nestedStack = new core.NestedStack(this, nestedStackId, { + parameters: nestedStackProps.Parameters, + notificationArns: nestedStackProps.NotificationArns, + timeout: nestedStackProps.Timeout, + }); + + // we know this is never undefined for nested stacks + const nestedStackResource: core.CfnResource = nestedStack.nestedStackResource!; + // handle resource attributes + const cfnOptions = nestedStackResource.cfnOptions; + cfnOptions.metadata = cfnParser.parseValue(nestedStackAttributes.Metadata); + cfnOptions.deletionPolicy = cfnParser.parseDeletionPolicy(nestedStackAttributes.DeletionPolicy); + cfnOptions.updateReplacePolicy = cfnParser.parseDeletionPolicy(nestedStackAttributes.UpdateReplacePolicy); + // handle DependsOn + nestedStackAttributes.DependsOn = nestedStackAttributes.DependsOn ?? []; + const dependencies: string[] = Array.isArray(nestedStackAttributes.DependsOn) ? + nestedStackAttributes.DependsOn : [nestedStackAttributes.DependsOn]; + for (const dep of dependencies) { + const depResource = finder.findResource(dep); + if (!depResource) { + throw new Error(`nested stack '${nestedStackId}' depends on '${dep}' that doesn't exist`); + } + nestedStackResource.node.addDependency(depResource); + } + // handle Condition + if (nestedStackAttributes.Condition) { + const condition = finder.findCondition(nestedStackAttributes.Condition); + if (!condition) { + throw new Error(`nested stack '${nestedStackId}' uses Condition '${nestedStackAttributes.Condition}' that doesn't exist`); + } + cfnOptions.condition = condition; + } + + const propStack = this.nestedStacksToInclude[nestedStackId]; + const template = new CfnInclude(nestedStack, nestedStackId, { + templateFile: propStack.templateFile, + nestedStacks: propStack.nestedStacks, + }); + const includedStack: IncludedNestedStack = { stack: nestedStack, includedTemplate: template }; + this.nestedStacks[nestedStackId] = includedStack; + + return nestedStackResource; + } } diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index b2630373f35b9..abf32289ebd73 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -37,6 +37,7 @@ "watch": "cdk-watch", "lint": "cdk-lint", "test": "cdk-test", + "integ": "cdk-integ", "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", @@ -305,9 +306,10 @@ "@types/jest": "^26.0.4", "@types/yaml": "1.2.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "jest": "^25.4.0", "pkglint": "0.0.0", - "ts-jest": "^26.1.2" + "ts-jest": "^26.1.1" }, "bundledDependencies": [ "yaml" @@ -327,6 +329,11 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "props-no-cfn-types:@aws-cdk/cloudformation-include.CfnIncludeProps.nestedStacks" + ] + }, "stability": "experimental", "maturity": "experimental", "awscdkio": { diff --git a/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.expected.json b/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.expected.json new file mode 100644 index 0000000000000..1a8759a8256de --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.expected.json @@ -0,0 +1,72 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0C" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + } + } + }, + "Parameters": { + "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0C": { + "Type": "String", + "Description": "S3 bucket for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"" + }, + "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2": { + "Type": "String", + "Description": "S3 key for asset version \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"" + }, + "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50ArtifactHash9C417847": { + "Type": "String", + "Description": "Artifact hash for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts b/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts new file mode 100644 index 0000000000000..5ccd876c328db --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts @@ -0,0 +1,17 @@ +import * as core from '@aws-cdk/core'; +import * as inc from '../lib'; + +const app = new core.App(); + +const stack = new core.Stack(app, 'ParentStack'); + +new inc.CfnInclude(stack, 'ParentStack', { + templateFile: 'test-templates/nested/parent-one-child.json', + nestedStacks: { + ChildStack: { + templateFile: 'test-templates/nested/grandchild-import-stack.json', + }, + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index 0d1ba70795e5d..765b0a7de265e 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -1,7 +1,7 @@ +import * as path from 'path'; import { SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as core from '@aws-cdk/core'; -import * as path from 'path'; import * as inc from '../lib'; describe('CDK Include', () => { diff --git a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts new file mode 100644 index 0000000000000..0d16cdfa057cc --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts @@ -0,0 +1,588 @@ +import * as path from 'path'; +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as core from '@aws-cdk/core'; +import * as inc from '../lib'; +import * as futils from '../lib/file-utils'; + +/* eslint-disable quote-props */ +/* eslint-disable quotes */ + +describe('CDK Include', () => { + let stack: core.Stack; + + beforeEach(() => { + stack = new core.Stack(); + }); + + test('can ingest a template with one child', () => { + const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-one-child.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + + const childStack = parentTemplate.getNestedStack('ChildStack'); + expect(childStack.stack).toMatchTemplate( + loadTestFileToJsObject('grandchild-import-stack.json'), + ); + }); + + test('can ingest a template with two children', () => { + const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-two-children.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + 'AnotherChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + + const childStack = parentTemplate.getNestedStack('ChildStack'); + const anotherChildStack = parentTemplate.getNestedStack('AnotherChildStack'); + expect(childStack.stack).toMatchTemplate( + loadTestFileToJsObject('grandchild-import-stack.json'), + ); + + expect(anotherChildStack.stack).toMatchTemplate( + loadTestFileToJsObject('grandchild-import-stack.json'), + ); + }); + + 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: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + nestedStacks: { + 'GrandChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }, + }, + }); + + const childStack = parentTemplate.getNestedStack('ChildStack'); + const grandChildStack = childStack.includedTemplate.getNestedStack('GrandChildStack'); + expect(childStack.stack).toMatchTemplate( + loadTestFileToJsObject('child-import-stack.expected.json'), + ); + + expect(grandChildStack.stack).toMatchTemplate( + loadTestFileToJsObject('grandchild-import-stack.json'), + ); + }); + + test('throws an error when provided a nested stack that is not present in the template', () => { + expect(() => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-two-children.json'), + nestedStacks: { + 'FakeStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + }).toThrow(/Nested Stack with logical ID 'FakeStack' was not found in the template/); + }); + + test('throws an exception when NestedStacks contains an ID that is not a CloudFormation::Stack in the template', () => { + expect(() => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('child-import-stack.json'), + nestedStacks: { + 'BucketImport': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + }).toThrow(/Nested Stack with logical ID 'BucketImport' is not an AWS::CloudFormation::Stack resource/); + }); + + test('throws an exception when the nestedStack resource uses the CreationPolicy attribute', () => { + expect(() => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-creation-policy.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + }).toThrow(/CreationPolicy is not supported by the AWS::CloudFormation::Stack resource/); + }); + + test('throws an exception when the nested stack resource uses the UpdatePolicy attribute', () => { + expect(() => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-update-policy.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + }).toThrow(/UpdatePolicy is not supported by the AWS::CloudFormation::Stack resource/); + }); + + test('throws an exception when a nested stack refers to a Condition that does not exist in the template', () => { + expect(() => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-invalid-condition.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + }).toThrow(/nested stack 'ChildStack' uses Condition 'FakeCondition' that doesn't exist/); + }); + + test('throws an exception when a nested stacks depends on a resource that does not exist in the template', () => { + expect(() => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-bad-depends-on.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + }).toThrow(/nested stack 'ChildStack' depends on 'AFakeResource' that doesn't exist/); + }); + + test('can modify resources in nested stacks', () => { + const parent = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('child-import-stack.json'), + nestedStacks: { + 'GrandChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + + const childTemplate = parent.getNestedStack('GrandChildStack').includedTemplate; + const bucket = childTemplate.getResource('BucketImport') as s3.CfnBucket; + + bucket.bucketName = 'modified-bucket-name'; + + expect(childTemplate.stack).toHaveResource('AWS::S3::Bucket', { BucketName: 'modified-bucket-name' }); + }); + + test('can use a condition', () => { + const parent = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-valid-condition.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + + const alwaysFalseCondition = parent.getCondition('AlwaysFalseCond'); + + expect(parent.getResource('ChildStack').cfnOptions.condition).toBe(alwaysFalseCondition); + }); + + test('asset parameters generated in parent and child are identical', () => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-one-child.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }); + + const assetParam = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0C'; + const assetParamKey = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2'; + expect(stack).toMatchTemplate({ + "Parameters": { + [assetParam]: { + "Type": "String", + "Description": "S3 bucket for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + }, + [assetParamKey]: { + "Type": "String", + "Description": "S3 key for asset version \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + }, + "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50ArtifactHash9C417847": { + "Type": "String", + "Description": "Artifact hash for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + }, + }, + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ "", [ + "https://s3.", + { "Ref": "AWS::Region" }, + ".", + { "Ref": "AWS::URLSuffix" }, + "/", + { "Ref": assetParam }, + "/", + { "Fn::Select": [ + 0, + { "Fn::Split": [ + "||", + { "Ref": assetParamKey }, + ]}, + ]}, + { "Fn::Select": [ + 1, + { "Fn::Split": [ + "||", + { "Ref": assetParamKey }, + ]}, + ]}, + ]], + }, + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name", + }, + }, + }, + }, + }); + }); + + test('templates with nested stacks that were not provided in the nestedStacks property are left unmodified', () => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-two-children.json'), + }); + + expect(stack).toMatchTemplate(loadTestFileToJsObject('parent-two-children.json')); + }); + + 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: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + + expect(() => { + parentTemplate.getNestedStack('FakeStack'); + }).toThrow(/Nested Stack with logical ID 'FakeStack' was not found/); + }); + + 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: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + + const childTemplate = parentTemplate.getNestedStack('ChildStack').includedTemplate; + + expect(() => { + childTemplate.getNestedStack('BucketImport'); + }).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', () => { + const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-two-children.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + + expect(() => { + parentTemplate.getNestedStack('AnotherChildStack'); + }).toThrow(/Nested Stack 'AnotherChildStack' was not included in the nestedStacks 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: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + const cfnBucket = parentTemplate.getResource('Bucket'); + cfnBucket.overrideLogicalId('DifferentBucket'); + const parameter = parentTemplate.getParameter('Param'); + parameter.overrideLogicalId('DifferentParameter'); + + expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + "Parameters": { + "Param1": { + "Ref": "DifferentParameter", + }, + "Param2": { + "Fn::GetAtt": ["DifferentBucket", "Arn"], + }, + }, + }); + }); + + test('returns the CfnStack object from getResource() for a nested stack that was not in the nestedStacks property', () => { + const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-two-children.json'), + }); + + const childStack1 = cfnTemplate.getResource('ChildStack'); + + expect(childStack1).toBeInstanceOf(core.CfnStack); + }); + + test('returns the CfnStack object from getResource() for a nested stack that was in the nestedStacks property', () => { + const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-one-child.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + + const childStack1 = cfnTemplate.getResource('ChildStack'); + + expect(childStack1).toBeInstanceOf(core.CfnStack); + }); + + 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: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + 'AnotherChildStack': { + templateFile: testTemplateFilePath('child-import-stack.json'), + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + "Metadata": { + "Property1": "Value1", + }, + "DeletionPolicy": "Retain", + "DependsOn": [ + "AnotherChildStack", + ], + "UpdateReplacePolicy": "Retain", + }, ResourcePart.CompleteDefinition); + + cfnTemplate.getNestedStack('AnotherChildStack'); + }); + + test('correctly parses NotificationsARNs, Timeout', () => { + new inc.CfnInclude(stack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-with-attributes.json'), + }); + + expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json", + "NotificationARNs": ["arn1"], + "TimeoutInMinutes": 5, + }); + }); + + describe('for a parent stack with children and grandchildren', () => { + let assetStack: core.Stack; + let parentTemplate: inc.CfnInclude; + let child: inc.IncludedNestedStack; + let grandChild: inc.IncludedNestedStack; + + let parentBucketParam: string; + let parentKeyParam: string; + let grandChildBucketParam: string; + let grandChildKeyParam: string; + + let childBucketParam: string; + let childKeyParam: string; + + beforeAll(() => { + assetStack = new core.Stack(); + parentTemplate = new inc.CfnInclude(assetStack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-one-child.json'), + nestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-no-bucket.json'), + nestedStacks: { + 'GrandChildStack': { + templateFile: testTemplateFilePath('grandchild-import-stack.json'), + }, + }, + }, + }, + }); + + child = parentTemplate.getNestedStack('ChildStack'); + grandChild = child.includedTemplate.getNestedStack('GrandChildStack'); + + parentBucketParam = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0C'; + parentKeyParam = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2'; + grandChildBucketParam = 'referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0CRef'; + grandChildKeyParam = 'referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2Ref'; + + childBucketParam = 'AssetParameters891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5S3Bucket23278F13'; + childKeyParam = 'AssetParameters891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5S3VersionKey7316205A'; + }); + + test('correctly creates parameters in the parent stack, and passes them to the child stack', () => { + expect(assetStack).toMatchTemplate({ + "Parameters": { + [parentBucketParam]: { + "Type": "String", + "Description": "S3 bucket for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + }, + [parentKeyParam]: { + "Type": "String", + "Description": "S3 key for asset version \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + }, + "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50ArtifactHash9C417847": { + "Type": "String", + "Description": "Artifact hash for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + }, + [childBucketParam]: { + "Type": "String", + "Description": "S3 bucket for asset \"891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5\"", + }, + [childKeyParam]: { + "Type": "String", + "Description": "S3 key for asset version \"891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5\"", + }, + "AssetParameters891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5ArtifactHashA1DE5198": { + "Type": "String", + "Description": "Artifact hash for asset \"891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5\"", + }, + }, + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ "", [ + "https://s3.", + { "Ref": "AWS::Region" }, + ".", + { "Ref": "AWS::URLSuffix" }, + "/", + { "Ref": childBucketParam }, + "/", + { "Fn::Select": [ + 0, + { "Fn::Split": [ + "||", + { "Ref": childKeyParam }, + ]}, + ]}, + { "Fn::Select": [ + 1, + { "Fn::Split": [ + "||", + { "Ref": childKeyParam }, + ]}, + ]}, + ]], + }, + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name", + [grandChildBucketParam]: { + "Ref": parentBucketParam, + }, + [grandChildKeyParam]: { + "Ref": parentKeyParam, + }, + }, + }, + }, + }, + }); + }); + + test('correctly creates parameters in the child stack, and passes them to the grandchild stack', () => { + expect(child.stack).toMatchTemplate({ + "Parameters": { + "MyBucketParameter": { + "Type": "String", + "Default": "default-bucket-param-name", + }, + [grandChildBucketParam]: { + "Type": "String", + }, + [grandChildKeyParam]: { + "Type": "String", + }, + }, + "Resources": { + "GrandChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ "", [ + "https://s3.", + { "Ref": "AWS::Region" }, + ".", + { "Ref": "AWS::URLSuffix" }, + "/", + { "Ref": grandChildBucketParam }, + "/", + { "Fn::Select": [ + 0, + { "Fn::Split": [ + "||", + { "Ref": grandChildKeyParam }, + ]}, + ]}, + { + "Fn::Select": [ + 1, + { "Fn::Split": [ + "||", + { "Ref": grandChildKeyParam }, + ]}, + ], + }, + ]], + }, + "Parameters": { + "MyBucketParameter": "some-other-bucket-name", + }, + }, + }, + }, + }); + }); + + test('leaves grandchild stack unmodified', () => { + expect(grandChild.stack).toMatchTemplate( + loadTestFileToJsObject('grandchild-import-stack.json'), + ); + }); + }); +}); + +function loadTestFileToJsObject(testTemplate: string): any { + return futils.readJsonSync(testTemplateFilePath(testTemplate)); +} + +function testTemplateFilePath(testTemplate: string) { + return path.join(__dirname, 'test-templates/nested', testTemplate); +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.expected.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.expected.json new file mode 100644 index 0000000000000..d1edb5eabf11c --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.expected.json @@ -0,0 +1,78 @@ +{ + "Parameters": { + "MyBucketParameter": { + "Type": "String", + "Default": "default-bucket-param-name" + }, + "referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0CRef": { + "Type": "String" + }, + "referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2Ref": { + "Type": "String" + } + }, + "Resources": { + "GrandChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0CRef" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2Ref" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2Ref" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "MyBucketParameter": "some-other-bucket-name" + } + } + }, + "BucketImport": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "MyBucketParameter" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.json new file mode 100644 index 0000000000000..25e2e5f9bf183 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-import-stack.json @@ -0,0 +1,27 @@ +{ + "Parameters": { + "MyBucketParameter": { + "Type": "String", + "Default": "default-bucket-param-name" + } + }, + "Resources": { + "BucketImport": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "MyBucketParameter" + } + } + }, + "GrandChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-other-bucket-name" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-no-bucket.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-no-bucket.json new file mode 100644 index 0000000000000..63ede414bbdaa --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/child-no-bucket.json @@ -0,0 +1,19 @@ +{ + "Parameters": { + "MyBucketParameter": { + "Type": "String", + "Default": "default-bucket-param-name" + } + }, + "Resources": { + "GrandChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-other-bucket-name" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/cross-stack-refs.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/cross-stack-refs.json new file mode 100644 index 0000000000000..d51bd69c1a26c --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/cross-stack-refs.json @@ -0,0 +1,22 @@ +{ + "Parameters": { + "Param": { + "Type": "String" + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket" + }, + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json", + "Parameters": { + "Param1": { "Ref": "Param" }, + "Param2": { "Fn::GetAtt": ["Bucket", "Arn"] } + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/grandchild-import-stack.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/grandchild-import-stack.json new file mode 100644 index 0000000000000..cf8b9a7953356 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/grandchild-import-stack.json @@ -0,0 +1,26 @@ +{ + "Parameters": { + "MyBucketParameter": { + "Type": "String", + "Default": "default-bucket-param-name" + } + }, + "Resources": { + "BucketImport": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Join": [ + "-", + [ + "bucket-name-prefix", + { + "Ref": "MyBucketParameter" + } + ] + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.expected.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.expected.json new file mode 100644 index 0000000000000..e29a1437b87ff --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.expected.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "NestedStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7S3BucketC8A1BF52" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7S3VersionKeyA9E03E19" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7S3VersionKeyA9E03E19" + } + ] + } + ] + } + ] + ] + } + } + } + }, + "Parameters": { + "AssetParameters6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7S3BucketC8A1BF52": { + "Type": "String", + "Description": "S3 bucket for asset \"6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7\"" + }, + "AssetParameters6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7S3VersionKeyA9E03E19": { + "Type": "String", + "Description": "S3 key for asset version \"6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7\"" + }, + "AssetParameters6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7ArtifactHash605B2835": { + "Type": "String", + "Description": "Artifact hash for asset \"6b884775090ed88cd1a143f64442a92a6c34eaeff3857976d15ef2e3beee05d7\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.json new file mode 100644 index 0000000000000..af762ed14cb89 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/only-nested-stack.json @@ -0,0 +1,10 @@ +{ + "Resources": { + "NestedStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "doesnt-matter" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-bad-depends-on.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-bad-depends-on.json new file mode 100644 index 0000000000000..badd2f7d78f9e --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-bad-depends-on.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json" + }, + "DependsOn": [ + "AFakeResource" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-creation-policy.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-creation-policy.json new file mode 100644 index 0000000000000..1143271208967 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-creation-policy.json @@ -0,0 +1,15 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + }, + "CreationPolicy": { + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-invalid-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-invalid-condition.json new file mode 100644 index 0000000000000..5d042b11b02b3 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-invalid-condition.json @@ -0,0 +1,14 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + }, + "Condition": "FakeCondition" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-one-child.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-one-child.json new file mode 100644 index 0000000000000..1fd331f53213f --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-one-child.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-two-children.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-two-children.json new file mode 100644 index 0000000000000..1ed2a9403afb6 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-two-children.json @@ -0,0 +1,22 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + } + }, + "AnotherChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json", + "Parameters": { + "MyBucketParameter": "another-magic-bucket-name" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-update-policy.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-update-policy.json new file mode 100644 index 0000000000000..8e4daa8f65014 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-update-policy.json @@ -0,0 +1,15 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + }, + "UpdatePolicy": { + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-valid-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-valid-condition.json new file mode 100644 index 0000000000000..6f7ebee7b51b0 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-valid-condition.json @@ -0,0 +1,24 @@ +{ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "completely-made-up-region" + ] + } + }, + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/grandchild-import-stack.json", + "Parameters": { + "MyBucketParameter": "some-magic-bucket-name" + } + }, + "Condition": "AlwaysFalseCond" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-with-attributes.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-with-attributes.json new file mode 100644 index 0000000000000..c12bd223063c1 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/parent-with-attributes.json @@ -0,0 +1,26 @@ +{ + "Resources": { + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json" + }, + "DependsOn": [ + "AnotherChildStack" + ], + "Metadata": { + "Property1": "Value1" + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" + }, + "AnotherChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-import-stack.json", + "NotificationARNs": [ "arn1" ], + "TimeoutInMinutes": 5 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index cfb69bb33f7c0..83b2370db5fac 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -1,9 +1,9 @@ +import * as path from 'path'; import { ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as core from '@aws-cdk/core'; -import * as path from 'path'; import * as inc from '../lib'; import * as futils from '../lib/file-utils'; diff --git a/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts index 9f0fdc4834828..01bba1610a9b5 100644 --- a/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts @@ -1,6 +1,6 @@ +import * as path from 'path'; import '@aws-cdk/assert/jest'; import * as core from '@aws-cdk/core'; -import * as path from 'path'; import * as inc from '../lib'; import * as futils from '../lib/file-utils';