diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts index 1d351364e019d..b6c9ba4ba39cd 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts @@ -98,6 +98,13 @@ export interface ArtifactManifest { * @default - no properties. */ readonly properties?: ArtifactProperties; + + /** + * A string that represents this artifact. Should only be used in user interfaces. + * + * @default - no display name + */ + readonly displayName?: string; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 2ed400edff3ca..38c7538f38384 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -77,6 +77,10 @@ "$ref": "#/definitions/NestedCloudAssemblyProperties" } ] + }, + "displayName": { + "description": "A string that represents this artifact. Should only be used in user interfaces. (Default - no display name)", + "type": "string" } }, "required": [ diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 193f97fba499d..1829f904a3c7a 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"9.0.0"} +{"version":"10.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts index 233490968b31d..c5d88c9e76686 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts @@ -57,6 +57,7 @@ export function addStackArtifactToAssembly( properties, dependencies: deps.length > 0 ? deps : undefined, metadata: Object.keys(meta).length > 0 ? meta : undefined, + displayName: stack.node.path, }); } diff --git a/packages/@aws-cdk/core/test/synthesis.test.ts b/packages/@aws-cdk/core/test/synthesis.test.ts index 8b5200c1dfb72..77c8c306ef81f 100644 --- a/packages/@aws-cdk/core/test/synthesis.test.ts +++ b/packages/@aws-cdk/core/test/synthesis.test.ts @@ -105,6 +105,7 @@ nodeunitShim({ type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', properties: { templateFile: 'one-stack.template.json' }, + displayName: 'one-stack', }, }, }); diff --git a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts index 5a57b9f36c1eb..7f4e5a899983a 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts @@ -142,6 +142,15 @@ export class CloudArtifact { return messages; } + + /** + * An identifier that shows where this artifact is located in the tree + * of nested assemblies, based on their manifests. Defaults to the normal + * id. Should only be used in user interfaces. + */ + public get hierarchicalId(): string { + return this.manifest.displayName ?? this.id; + } } // needs to be defined at the end to avoid a cyclic dependency diff --git a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts index 08d8fcbcfbbc0..400981cea7054 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts @@ -108,7 +108,8 @@ export class CloudAssembly { * @returns a `CloudFormationStackArtifact` object. */ public getStackArtifact(artifactId: string): CloudFormationStackArtifact { - const artifact = this.tryGetArtifact(artifactId); + const artifact = this.tryGetArtifactRecursively(artifactId); + if (!artifact) { throw new Error(`Unable to find artifact with id "${artifactId}"`); } @@ -120,6 +121,27 @@ export class CloudAssembly { return artifact; } + private tryGetArtifactRecursively(artifactId: string): CloudArtifact | undefined { + return this.stacksRecursively.find(a => a.id === artifactId); + } + + /** + * Returns all the stacks, including the ones in nested assemblies + */ + public get stacksRecursively(): CloudFormationStackArtifact[] { + function search(stackArtifacts: CloudFormationStackArtifact[], assemblies: CloudAssembly[]): CloudFormationStackArtifact[] { + if (assemblies.length === 0) { + return stackArtifacts; + } + + const [head, ...tail] = assemblies; + const nestedAssemblies = head.nestedAssemblies.map(asm => asm.nestedAssembly); + return search(stackArtifacts.concat(head.stacks), tail.concat(nestedAssemblies)); + }; + + return search([], [this]); + } + /** * Returns a nested assembly artifact. * diff --git a/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts b/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts index 6f477be5185ea..ddd7538308ec3 100644 --- a/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts +++ b/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts @@ -151,3 +151,14 @@ test('can read assembly with asset manifest', () => { expect(assembly.stacks).toHaveLength(1); expect(assembly.artifacts).toHaveLength(2); }); + +test('getStackArtifact retrieves a stack by artifact id from a nested assembly', () => { + const assembly = new CloudAssembly(path.join(FIXTURES, 'nested-assemblies')); + + expect(assembly.getStackArtifact('topLevelStack').stackName).toEqual('topLevelStack'); + expect(assembly.getStackArtifact('stack1').stackName).toEqual('first-stack'); + expect(assembly.getStackArtifact('stack2').stackName).toEqual('second-stack'); + expect(assembly.getStackArtifact('topLevelStack').id).toEqual('topLevelStack'); + expect(assembly.getStackArtifact('stack1').id).toEqual('stack1'); + expect(assembly.getStackArtifact('stack2').id).toEqual('stack2'); +}); diff --git a/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/manifest.json b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/manifest.json new file mode 100644 index 0000000000000..b79e280d6a1fb --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/manifest.json @@ -0,0 +1,21 @@ +{ + "version": "0.0.0", + "artifacts": { + "subassembly": { + "type": "cdk:cloud-assembly", + "properties": { + "directoryName": "subassembly", + "displayName": "subassembly" + } + }, + "topLevelStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://111111111111/us-east-1", + "properties": { + "templateFile": "topLevelStack.template.json", + "stackName": "topLevelStack" + }, + "displayName": "topLevelStack" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/manifest.json b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/manifest.json new file mode 100644 index 0000000000000..703e640daea91 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/manifest.json @@ -0,0 +1,20 @@ +{ + "version": "0.0.0", + "artifacts": { + "subsubassembly": { + "type": "cdk:cloud-assembly", + "properties": { + "directoryName": "subsubassembly", + "displayName": "subsubassembly" + } + }, + "stack1": { + "type": "aws:cloudformation:stack", + "environment": "aws://37736633/us-region-1", + "properties": { + "templateFile": "template.json", + "stackName": "first-stack" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/subsubassembly/manifest.json b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/subsubassembly/manifest.json new file mode 100644 index 0000000000000..4166babad9831 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/subsubassembly/manifest.json @@ -0,0 +1,13 @@ +{ + "version": "0.0.0", + "artifacts": { + "stack2": { + "type": "aws:cloudformation:stack", + "environment": "aws://37736633/us-region-1", + "properties": { + "templateFile": "template.2.json", + "stackName": "second-stack" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/subsubassembly/template.2.json b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/subsubassembly/template.2.json new file mode 100644 index 0000000000000..284fd64cffc21 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/subsubassembly/template.2.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "MyBucket": { + "Type": "AWS::S3::Bucket" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/template.json b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/template.json new file mode 100644 index 0000000000000..284fd64cffc21 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/subassembly/template.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "MyBucket": { + "Type": "AWS::S3::Bucket" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/topLevelStack.template.json b/packages/@aws-cdk/cx-api/test/fixtures/nested-assemblies/topLevelStack.template.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/cx-api/test/stack-artifact.test.ts b/packages/@aws-cdk/cx-api/test/stack-artifact.test.ts index 1f4591f00177a..baba32bb7b573 100644 --- a/packages/@aws-cdk/cx-api/test/stack-artifact.test.ts +++ b/packages/@aws-cdk/cx-api/test/stack-artifact.test.ts @@ -130,3 +130,30 @@ test('read tags from stack metadata', () => { // THEN expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' }); }); + +test('user friendly id is the assembly display name', () => { + // GIVEN + builder.addArtifact('Stack', { + ...stackBase, + displayName: 'some/path/to/the/stack', + }); + + // WHEN + const assembly = builder.buildAssembly(); + + // THEN + expect(assembly.getStackByName('Stack').hierarchicalId).toEqual('some/path/to/the/stack'); +}); + +test('user friendly id is the id itself if no display name is given', () => { + // GIVEN + builder.addArtifact('Stack', { + ...stackBase, + }); + + // WHEN + const assembly = builder.buildAssembly(); + + // THEN + expect(assembly.getStackByName('Stack').hierarchicalId).toEqual('Stack'); +}); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 41dd4c75fbe2a..ece1ff5450081 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -158,9 +158,9 @@ and always deploy the stack. You can have multiple stacks in a cdk app. An example can be found in [how to create multiple stacks](https://docs.aws.amazon.com/cdk/latest/guide/stack_how_to_create_multiple_stacks.html). -In order to deploy them, you can list the stacks you want to deploy. +In order to deploy them, you can list the stacks you want to deploy. If your application contains pipeline stacks, the `cdk list` command will show stack names as paths, showing where they are in the pipeline hierarchy (e.g., `PipelineStack`, `PipelineStack/Prod`, `PipelineStack/Prod/MyService` etc). -If you want to deploy all of them, you can use the flag `--all` or the wildcard `*` to deploy all stacks in an app. +If you want to deploy all of them, you can use the flag `--all` or the wildcard `*` to deploy all stacks in an app. Please note that, if you have a hierarchy of stacks as described above, `--all` and `*` will only match the stacks on the top level. If you want to match all the stacks in the hierarchy, use `**`. You can also combine these patterns. For example, if you want to deploy all stacks in the `Prod` stage, you can use `cdk deploy PipelineStack/Prod/**`. #### Parameters diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index fb96c549f7fc7..6c62bdbd7599b 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -1,7 +1,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as colors from 'colors/safe'; import * as minimatch from 'minimatch'; +import * as semver from 'semver'; import { error, print, warning } from '../../logging'; +import { versionNumber } from '../../version'; export enum DefaultSelection { /** @@ -16,7 +18,13 @@ export enum DefaultSelection { OnlySingle = 'single', /** - * If no selectors are provided, returns all stacks in the app. + * Returns all stacks in the main (top level) assembly only. + */ + MainAssembly = 'main', + + /** + * If no selectors are provided, returns all stacks in the app, + * including stacks inside nested assemblies. */ AllStacks = 'all', } @@ -71,20 +79,23 @@ export class CloudAssembly { selectors = selectors.filter(s => s != null); // filter null/undefined selectors = [...new Set(selectors)]; // make them unique - const stacks = this.assembly.stacks; + const stacks = this.assembly.stacksRecursively; if (stacks.length === 0) { throw new Error('This app contains no stacks'); } if (selectors.length === 0) { + const topLevelStacks = this.assembly.stacks; switch (options.defaultBehavior) { + case DefaultSelection.MainAssembly: + return new StackCollection(this, topLevelStacks); case DefaultSelection.AllStacks: return new StackCollection(this, stacks); case DefaultSelection.None: return new StackCollection(this, []); case DefaultSelection.OnlySingle: - if (stacks.length === 1) { - return new StackCollection(this, stacks); + if (topLevelStacks.length === 1) { + return new StackCollection(this, topLevelStacks); } else { throw new Error('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' + `Stacks: ${stacks.map(x => x.id).join(' ')}`); @@ -96,7 +107,7 @@ export class CloudAssembly { const allStacks = new Map(); for (const stack of stacks) { - allStacks.set(stack.id, stack); + allStacks.set(stack.hierarchicalId, stack); } // For every selector argument, pick stacks from the list. @@ -105,8 +116,14 @@ export class CloudAssembly { let found = false; for (const stack of stacks) { - if (minimatch(stack.id, pattern) && !selectedStacks.has(stack.id)) { - selectedStacks.set(stack.id, stack); + const hierarchicalId = stack.hierarchicalId; + if (minimatch(hierarchicalId, pattern) && !selectedStacks.has(hierarchicalId)) { + selectedStacks.set(hierarchicalId, stack); + found = true; + } else if (minimatch(stack.id, pattern) && !selectedStacks.has(hierarchicalId) && semver.major(versionNumber()) < 2) { + warning('Selecting stack by identifier "%s". This identifier is deprecated and will be removed in v2. Please use "%s" instead.', colors.bold(stack.id), colors.bold(stack.hierarchicalId)); + warning('Run "cdk ls" to see a list of all stack identifiers'); + selectedStacks.set(hierarchicalId, stack); found = true; } } @@ -127,7 +144,7 @@ export class CloudAssembly { } // Filter original array because it is in the right order - const selectedList = stacks.filter(s => selectedStacks.has(s.id)); + const selectedList = stacks.filter(s => selectedStacks.has(s.hierarchicalId)); return new StackCollection(this, selectedList); } @@ -282,7 +299,7 @@ function includeUpstreamStacks( for (const stack of selectedStacks.values()) { // Select an additional stack if it's not selected yet and a dependency of a selected stack (and exists, obviously) - for (const dependencyId of stack.dependencies.map(x => x.id)) { + for (const dependencyId of stack.dependencies.map(x => x.manifest.displayName ?? x.id)) { if (!selectedStacks.has(dependencyId) && allStacks.has(dependencyId)) { added.push(dependencyId); selectedStacks.set(dependencyId, allStacks.get(dependencyId)!); diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 5491a3d221e24..f7585f6bface8 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -240,7 +240,7 @@ export class CdkToolkit { if (!options.force) { // eslint-disable-next-line max-len - const confirmed = await promptly.confirm(`Are you sure you want to delete: ${colors.blue(stacks.stackArtifacts.map(s => s.id).join(', '))} (y/n)?`); + const confirmed = await promptly.confirm(`Are you sure you want to delete: ${colors.blue(stacks.stackArtifacts.map(s => s.hierarchicalId).join(', '))} (y/n)?`); if (!confirmed) { return; } @@ -281,7 +281,7 @@ export class CdkToolkit { // just print stack IDs for (const stack of stacks.stackArtifacts) { - data(stack.id); + data(stack.hierarchicalId); } return 0; // exit-code @@ -396,7 +396,7 @@ export class CdkToolkit { const assembly = await this.assembly(); const stacks = await assembly.selectStacks(stackNames, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, - defaultBehavior: DefaultSelection.AllStacks, + defaultBehavior: DefaultSelection.MainAssembly, }); await this.validateStacks(stacks); diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index 6559718b9f51a..c85146d71e423 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -73,6 +73,50 @@ test('select behavior: repeat', async () => { expect(x.stackCount).toBe(1); }); +test('select behavior with nested assemblies: all', async () => { + // GIVEN + const cxasm = await testNestedCloudAssembly(); + + // WHEN + const x = await cxasm.selectStacks([], { defaultBehavior: DefaultSelection.AllStacks }); + + // THEN + expect(x.stackCount).toBe(3); +}); + +test('select behavior with nested assemblies: none', async () => { + // GIVEN + const cxasm = await testNestedCloudAssembly(); + + // WHEN + const x = await cxasm.selectStacks([], { defaultBehavior: DefaultSelection.None }); + + // THEN + expect(x.stackCount).toBe(0); +}); + +test('select behavior with nested assemblies: single', async () => { + // GIVEN + const cxasm = await testNestedCloudAssembly(); + + // WHEN + await expect(cxasm.selectStacks([], { defaultBehavior: DefaultSelection.OnlySingle })) + .rejects.toThrow('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`'); +}); + +test('select behavior with nested assemblies: repeat', async() => { + // GIVEN + const cxasm = await testNestedCloudAssembly(); + + // WHEN + const x = await cxasm.selectStacks(['withouterrors', 'withouterrors', 'nested'], { + defaultBehavior: DefaultSelection.AllStacks, + }); + + // THEN + expect(x.stackCount).toBe(2); +}); + async function testCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) { const cloudExec = new MockCloudExecutable({ stacks: [{ @@ -97,3 +141,43 @@ async function testCloudAssembly({ env }: { env?: string, versionReporting?: boo return cloudExec.synthesize(); } + +async function testNestedCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) { + const cloudExec = new MockCloudExecutable({ + stacks: [{ + stackName: 'withouterrors', + env, + template: { resource: 'noerrorresource' }, + }, + { + stackName: 'witherrors', + env, + template: { resource: 'errorresource' }, + metadata: { + '/resource': [ + { + type: cxschema.ArtifactMetadataEntryType.ERROR, + data: 'this is an error', + }, + ], + }, + }], + nestedAssemblies: [{ + stacks: [{ + stackName: 'nested', + env, + template: { resource: 'nestederror' }, + metadata: { + '/resource': [ + { + type: cxschema.ArtifactMetadataEntryType.ERROR, + data: 'this is another error', + }, + ], + }, + }], + }], + }); + + return cloudExec.synthesize(); +} diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 9266d9bc10646..444025ae8d16d 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -18,6 +18,9 @@ beforeEach(() => { MockStack.MOCK_STACK_A, MockStack.MOCK_STACK_B, ], + nestedAssemblies: [{ + stacks: [MockStack.MOCK_STACK_C], + }], }); }); @@ -30,6 +33,7 @@ function defaultToolkitSetup() { cloudFormation: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, 'Test-Stack-B': { Baz: 'Zinga!' }, + 'Test-Stack-C': { Baz: 'Zinga!' }, }), }); } @@ -44,6 +48,15 @@ describe('deploy', () => { await toolkit.deploy({ stackNames: ['Test-Stack-A', 'Test-Stack-B'] }); }); + test('with stacks all stacks specified as double wildcard', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + + // WHEN + await toolkit.deploy({ stackNames: ['**'] }); + }); + + test('with one stack specified', async () => { // GIVEN const toolkit = defaultToolkitSetup(); @@ -179,6 +192,22 @@ class MockStack { ], }, }; + public static readonly MOCK_STACK_C: TestStackArtifact = { + stackName: 'Test-Stack-C', + template: { Resources: { TempalteName: 'Test-Stack-C' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-C': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Baz', value: 'Zinga!' }, + ], + }, + ], + }, + displayName: 'Test-Stack-A/Test-Stack-C', + }; } class FakeCloudFormation extends CloudFormationDeployments { @@ -202,7 +231,7 @@ class FakeCloudFormation extends CloudFormationDeployments { } public deployStack(options: DeployStackOptions): Promise { - expect([MockStack.MOCK_STACK_A.stackName, MockStack.MOCK_STACK_B.stackName]) + expect([MockStack.MOCK_STACK_A.stackName, MockStack.MOCK_STACK_B.stackName, MockStack.MOCK_STACK_C.stackName]) .toContain(options.stack.stackName); expect(options.tags).toEqual(this.expectedTags[options.stack.stackName]); expect(options.notificationArns).toEqual(this.expectedNotificationArns); @@ -220,6 +249,8 @@ class FakeCloudFormation extends CloudFormationDeployments { return Promise.resolve({}); case MockStack.MOCK_STACK_B.stackName: return Promise.resolve({}); + case MockStack.MOCK_STACK_C.stackName: + return Promise.resolve({}); default: return Promise.reject(`Not an expected mock stack: ${stack.stackName}`); } diff --git a/packages/aws-cdk/test/util.ts b/packages/aws-cdk/test/util.ts index e15a1f6537354..060c75d29f63a 100644 --- a/packages/aws-cdk/test/util.ts +++ b/packages/aws-cdk/test/util.ts @@ -17,11 +17,13 @@ export interface TestStackArtifact { assets?: cxschema.AssetMetadataEntry[]; properties?: Partial; terminationProtection?: boolean; + displayName?: string; } export interface TestAssembly { stacks: TestStackArtifact[]; missing?: cxschema.MissingContext[]; + nestedAssemblies?: TestAssembly[]; } export class MockCloudExecutable extends CloudExecutable { @@ -47,9 +49,7 @@ function clone(obj: any) { return JSON.parse(JSON.stringify(obj)); } -export function testAssembly(assembly: TestAssembly): cxapi.CloudAssembly { - const builder = new cxapi.CloudAssemblyBuilder(); - +function addAttributes(assembly: TestAssembly, builder: cxapi.CloudAssemblyBuilder) { for (const stack of assembly.stacks) { const templateFile = `${stack.stackName}.template.json`; const template = stack.template ?? DEFAULT_FAKE_TEMPLATE; @@ -79,6 +79,20 @@ export function testAssembly(assembly: TestAssembly): cxapi.CloudAssembly { templateFile, terminationProtection: stack.terminationProtection, }, + displayName: stack.displayName, + }); + } +} + +export function testAssembly(assembly: TestAssembly): cxapi.CloudAssembly { + const builder = new cxapi.CloudAssemblyBuilder(); + addAttributes(assembly, builder); + + if (assembly.nestedAssemblies != null && assembly.nestedAssemblies.length > 0) { + assembly.nestedAssemblies?.forEach((nestedAssembly: TestAssembly, i: number) => { + const nestedAssemblyBuilder = builder.createNestedAssembly(`nested${i}`, `nested${i}`); + addAttributes(nestedAssembly, nestedAssemblyBuilder); + nestedAssemblyBuilder.buildAssembly(); }); }