diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index ed1117dc9dc94..a5524ae7a8e26 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -23,7 +23,8 @@ if (process.env.PACKAGE_LAYOUT_VERSION === '1') { aws_sns: sns, aws_sqs: sqs, aws_lambda: lambda, - aws_ecr_assets: docker + aws_ecr_assets: docker, + Stack } = require('aws-cdk-lib'); } @@ -65,6 +66,59 @@ class YourStack extends cdk.Stack { } } +class ListMultipleDependentStack extends Stack { + constructor(scope, id) { + super(scope, id); + + const dependentStack1 = new DependentStack1(this, 'DependentStack1'); + const dependentStack2 = new DependentStack2(this, 'DependentStack2'); + + this.addDependency(dependentStack1); + this.addDependency(dependentStack2); + } +} + +class DependentStack1 extends Stack { + constructor(scope, id) { + super(scope, id); + + } +} + +class DependentStack2 extends Stack { + constructor(scope, id) { + super(scope, id); + + } +} + +class ListStack extends Stack { + constructor(scope, id) { + super(scope, id); + + const dependentStack = new DependentStack(this, 'DependentStack'); + + this.addDependency(dependentStack); + } +} + +class DependentStack extends Stack { + constructor(scope, id) { + super(scope, id); + + const innerDependentStack = new InnerDependentStack(this, 'InnerDependentStack'); + + this.addDependency(innerDependentStack); + } +} + +class InnerDependentStack extends Stack { + constructor(scope, id) { + super(scope, id); + + } +} + class MigrateStack extends cdk.Stack { constructor(parent, id, props) { super(parent, id, props); @@ -498,6 +552,8 @@ switch (stackSet) { new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`); new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`); + new ListStack(app, `${stackPrefix}-list-stacks`) + new ListMultipleDependentStack(app, `${stackPrefix}-list-multiple-dependent-stacks`); new YourStack(app, `${stackPrefix}-termination-protection`, { terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false, diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index d22d636de2996..d3d5b12d94154 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -844,6 +844,139 @@ integTest('cdk ls', withDefaultFixture(async (fixture) => { } })); +/** + * Type to store stack dependencies recursively + */ +type DependencyDetails = { + id: string; + dependencies: DependencyDetails[]; +}; + +type StackDetails = { + id: string; + dependencies: DependencyDetails[]; +}; + +integTest('cdk ls --show-dependencies --json', withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls --show-dependencies --json'], { captureStderr: false }); + + const expectedStacks = [ + { + id: 'test-1', + dependencies: [], + }, + { + id: 'order-providing', + dependencies: [], + }, + { + id: 'order-consuming', + dependencies: [ + { + id: 'order-providing', + dependencies: [], + }, + ], + }, + { + id: 'with-nested-stack', + dependencies: [], + }, + { + id: 'list-stacks', + dependencies: [ + { + id: 'liststacksDependentStack', + dependencies: [ + { + id: 'liststacksDependentStackInnerDependentStack', + dependencies: [], + }, + ], + }, + ], + }, + { + id: 'list-multiple-dependent-stacks', + dependencies: [ + { + id: 'listmultipledependentstacksDependentStack1', + dependencies: [], + }, + { + id: 'listmultipledependentstacksDependentStack2', + dependencies: [], + }, + ], + }, + ]; + + function validateStackDependencies(stack: StackDetails) { + expect(listing).toContain(stack.id); + + function validateDependencies(dependencies: DependencyDetails[]) { + for (const dependency of dependencies) { + expect(listing).toContain(dependency.id); + if (dependency.dependencies.length > 0) { + validateDependencies(dependency.dependencies); + } + } + } + + if (stack.dependencies.length > 0) { + validateDependencies(stack.dependencies); + } + } + + for (const stack of expectedStacks) { + validateStackDependencies(stack); + } +})); + +integTest('cdk ls --show-dependencies --json --long', withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls --show-dependencies --json --long'], { captureStderr: false }); + + const expectedStacks = [ + { + id: 'order-providing', + name: 'order-providing', + enviroment: { + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region', + }, + dependencies: [], + }, + { + id: 'order-consuming', + name: 'order-consuming', + enviroment: { + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region', + }, + dependencies: [ + { + id: 'order-providing', + dependencies: [], + }, + ], + }, + ]; + + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack.id)); + expect(listing).toContain(fixture.fullStackName(stack.name)); + expect(listing).toContain(stack.enviroment.account); + expect(listing).toContain(stack.enviroment.name); + expect(listing).toContain(stack.enviroment.region); + for (const dependency of stack.dependencies) { + expect(listing).toContain(fixture.fullStackName(dependency.id)); + } + } + +})); + integTest('synthing a stage with errors leads to failure', withDefaultFixture(async (fixture) => { const output = await fixture.cdk(['synth'], { allowErrExit: true, diff --git a/packages/aws-cdk-lib/aws-codebuild/lib/project.ts b/packages/aws-cdk-lib/aws-codebuild/lib/project.ts index 213f90b28b2ec..8f32928af4965 100644 --- a/packages/aws-cdk-lib/aws-codebuild/lib/project.ts +++ b/packages/aws-cdk-lib/aws-codebuild/lib/project.ts @@ -1995,7 +1995,7 @@ export class WindowsBuildImage implements IBuildImage { /** * Corresponds to the standard CodeBuild image `aws/codebuild/windows-base:1.0`. * - * @deprecated `WindowsBuildImage.WIN_SERVER_CORE_2019_BASE_2_0` should be used instead. + * @deprecated `WindowsBuildImage.WIN_SERVER_CORE_2019_BASE_3_0` should be used instead. */ public static readonly WIN_SERVER_CORE_2016_BASE: IBuildImage = new WindowsBuildImage({ imageId: 'aws/codebuild/windows-base:1.0', @@ -2006,7 +2006,7 @@ export class WindowsBuildImage implements IBuildImage { * The standard CodeBuild image `aws/codebuild/windows-base:2.0`, which is * based off Windows Server Core 2016. * - * @deprecated `WindowsBuildImage.WIN_SERVER_CORE_2019_BASE_2_0` should be used instead. + * @deprecated `WindowsBuildImage.WIN_SERVER_CORE_2019_BASE_3_0` should be used instead. */ public static readonly WINDOWS_BASE_2_0: IBuildImage = new WindowsBuildImage({ imageId: 'aws/codebuild/windows-base:2.0', @@ -2033,6 +2033,16 @@ export class WindowsBuildImage implements IBuildImage { imageType: WindowsImageType.SERVER_2019, }); + /** + * The standard CodeBuild image `aws/codebuild/windows-base:2019-3.0`, which is + * based off Windows Server Core 2019. + */ + public static readonly WIN_SERVER_CORE_2019_BASE_3_0: IBuildImage = new WindowsBuildImage({ + imageId: 'aws/codebuild/windows-base:2019-3.0', + imagePullPrincipalType: ImagePullPrincipalType.CODEBUILD, + imageType: WindowsImageType.SERVER_2019, + }); + /** * @returns a Windows build image from a Docker Hub image. */ diff --git a/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts b/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts index d64fbf27b5397..fa4c3bceedcfe 100644 --- a/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts +++ b/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts @@ -798,6 +798,7 @@ describe('Environment', () => { ['Amazon Linux 4.0', codebuild.LinuxBuildImage.AMAZON_LINUX_2_4, 'aws/codebuild/amazonlinux2-x86_64-standard:4.0'], ['Amazon Linux 5.0', codebuild.LinuxBuildImage.AMAZON_LINUX_2_5, 'aws/codebuild/amazonlinux2-x86_64-standard:5.0'], ['Windows Server Core 2019 2.0', codebuild.WindowsBuildImage.WIN_SERVER_CORE_2019_BASE_2_0, 'aws/codebuild/windows-base:2019-2.0'], + ['Windows Server Core 2019 3.0', codebuild.WindowsBuildImage.WIN_SERVER_CORE_2019_BASE_3_0, 'aws/codebuild/windows-base:2019-3.0'], ])('has build image for %s', (_, buildImage, expected) => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 55a435a57df3a..42c3f9f6ec1f3 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -15,7 +15,7 @@ The AWS CDK Toolkit provides the `cdk` command-line interface that can be used t | ------------------------------------- | ---------------------------------------------------------------------------------- | | [`cdk docs`](#cdk-docs) | Access the online documentation | | [`cdk init`](#cdk-init) | Start a new CDK project (app or library) | -| [`cdk list`](#cdk-list) | List stacks in an application | +| [`cdk list`](#cdk-list) | List stacks and their dependencies in an application | | [`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s) | | [`cdk diff`](#cdk-diff) | Diff stacks against current state | | [`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account | @@ -74,7 +74,7 @@ $ cdk init lib --language=typescript ### `cdk list` -Lists the stacks modeled in the CDK app. +Lists the stacks and their dependencies modeled in the CDK app. ```console $ # List all stacks in the CDK app 'node bin/main.js' diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index ea3931fc6ddc3..3e721bb47e32a 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -20,6 +20,7 @@ import { StackActivityProgress } from './api/util/cloudformation/stack-activity- import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, parseSourceOptions, generateTemplate, FromScan, TemplateSourceOptions, GenerateTemplateOutput, CfnTemplateGeneratorProvider, writeMigrateJsonFile, buildGenertedTemplateOutput, buildCfnClient, appendWarningsToReadme, isThereAWarning } from './commands/migrate'; import { printSecurityDiff, printStackDiff, RequireApproval } from './diff'; import { ResourceImporter, removeNonImportResources } from './import'; +import { listStacks } from './list-stacks'; import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging'; import { deserializeStructure, serializeStructure } from './serialize'; import { Configuration, PROJECT_CONFIG } from './settings'; @@ -613,16 +614,37 @@ export class CdkToolkit { } } - public async list(selectors: string[], options: { long?: boolean; json?: boolean } = { }): Promise { - const stacks = await this.selectStacksForList(selectors); + public async list(selectors: string[], options: { long?: boolean; json?: boolean; showDeps?: boolean } = { }): Promise { + const stacks = await listStacks(this, { + selectors: selectors, + }); + + if (options.long && options.showDeps) { + data(serializeStructure(stacks, options.json ?? false)); + return 0; + } + + if (options.showDeps) { + const stackDeps = []; + + for (const stack of stacks) { + stackDeps.push({ + id: stack.id, + dependencies: stack.dependencies, + }); + } + + data(serializeStructure(stackDeps, options.json ?? false)); + return 0; + } - // if we are in "long" mode, emit the array as-is (JSON/YAML) if (options.long) { const long = []; - for (const stack of stacks.stackArtifacts) { + + for (const stack of stacks) { long.push({ - id: stack.hierarchicalId, - name: stack.stackName, + id: stack.id, + name: stack.name, environment: stack.environment, }); } @@ -631,8 +653,8 @@ export class CdkToolkit { } // just print stack IDs - for (const stack of stacks.stackArtifacts) { - data(stack.hierarchicalId); + for (const stack of stacks) { + data(stack.id); } return 0; // exit-code @@ -905,7 +927,7 @@ export class CdkToolkit { return assembly.stackById(stacks.firstStack.id); } - private assembly(cacheCloudAssembly?: boolean): Promise { + public assembly(cacheCloudAssembly?: boolean): Promise { return this.props.cloudExecutable.synthesize(cacheCloudAssembly); } diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index d7465e78693bf..27530c59ff215 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -87,7 +87,8 @@ async function parseCommandLineArguments(args: string[]) { .option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false }) .option('ci', { type: 'boolean', desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', default: process.env.CI !== undefined }) .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs - .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }), + .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }) + .option('show-dependencies', { type: 'boolean', default: false, alias: 'd', desc: 'Display stack dependency information for each stack' }), ) .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => yargs .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }) @@ -498,7 +499,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { + const assembly = await toolkit.assembly(); + + const stacks = await assembly.selectStacks({ + patterns: options.selectors, + }, { + extend: ExtendedStackSelection.Upstream, + defaultBehavior: DefaultSelection.AllStacks, + }); + + function calculateStackDependencies(collectionOfStacks: StackCollection): StackDetails[] { + const allData: StackDetails[] = []; + + for (const stack of collectionOfStacks.stackArtifacts) { + const data: StackDetails = { + id: stack.id, + name: stack.stackName, + environment: stack.environment, + dependencies: [], + }; + + for (const dependencyId of stack.dependencies.map(x => x.id)) { + if (dependencyId.includes('.assets')) { + continue; + } + + const depStack = assembly.stackById(dependencyId); + + if (depStack.stackArtifacts[0].dependencies.length > 0 && + depStack.stackArtifacts[0].dependencies.filter((dep) => !(dep.id).includes('.assets')).length > 0) { + + const stackWithDeps = calculateStackDependencies(depStack); + + for (const stackDetail of stackWithDeps) { + data.dependencies.push({ + id: stackDetail.id, + dependencies: stackDetail.dependencies, + }); + } + } else { + data.dependencies.push({ + id: depStack.stackArtifacts[0].id, + dependencies: [], + }); + } + } + + allData.push(data); + } + + return allData; + } + + return calculateStackDependencies(stacks); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/list-stacks.test.ts b/packages/aws-cdk/test/list-stacks.test.ts new file mode 100644 index 0000000000000..e36081e99c1d2 --- /dev/null +++ b/packages/aws-cdk/test/list-stacks.test.ts @@ -0,0 +1,430 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema';; +import { instanceMockFrom, MockCloudExecutable, TestStackArtifact } from './util'; +import { Bootstrapper } from '../lib/api/bootstrap'; +import { Deployments } from '../lib/api/deployments'; +import { CdkToolkit } from '../lib/cdk-toolkit'; +import { listStacks } from '../lib/list-stacks'; + +describe('list', () => { + let cloudFormation: jest.Mocked; + let bootstrapper: jest.Mocked; + + beforeEach(() => { + jest.resetAllMocks(); + + bootstrapper = instanceMockFrom(Bootstrapper); + bootstrapper.bootstrapEnvironment.mockResolvedValue({ noOp: false, outputs: {} } as any); + }); + + test('stacks with no dependencies', async () => { + let cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-B': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + }, + ], + }); + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: cloudFormation, + }); + + // WHEN + const workflow = await listStacks(toolkit, { selectors: ['Test-Stack-A', 'Test-Stack-B'] }); + + // THEN + expect(JSON.stringify(workflow)).toEqual(JSON.stringify([{ + id: 'Test-Stack-A', + name: 'Test-Stack-A', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [], + }, + { + id: 'Test-Stack-B', + name: 'Test-Stack-B', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [], + }])); + }); + + test('stacks with dependent stacks', async () => { + let cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-B': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-A'], + }, + ], + }); + + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: cloudFormation, + }); + + // WHEN + const workflow = await listStacks( toolkit, { selectors: ['Test-Stack-A', 'Test-Stack-B'] }); + + // THEN + expect(JSON.stringify(workflow)).toEqual(JSON.stringify([{ + id: 'Test-Stack-A', + name: 'Test-Stack-A', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [], + }, + { + id: 'Test-Stack-B', + name: 'Test-Stack-B', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [{ + id: 'Test-Stack-A', + dependencies: [], + }], + }])); + }); + + // In the context where we have a display name set to hieraricalId/stackName + // we would need to pass in the displayName to list the stacks. + test('stacks with dependent stacks and have display name set to hieraricalId/stackName', async () => { + let cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-B': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-A'], + displayName: 'Test-Stack-A/Test-Stack-B', + }, + ], + }); + + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: cloudFormation, + }); + + // WHEN + const workflow = await listStacks( toolkit, { selectors: ['Test-Stack-A', 'Test-Stack-A/Test-Stack-B'] }); + + // THEN + expect(JSON.stringify(workflow)).toEqual(JSON.stringify([{ + id: 'Test-Stack-A', + name: 'Test-Stack-A', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [], + }, + { + id: 'Test-Stack-B', + name: 'Test-Stack-B', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [{ + id: 'Test-Stack-A', + dependencies: [], + }], + }])); + }); + + test('stacks with nested dependencies', async () => { + let cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-B': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-A'], + }, + { + stackName: 'Test-Stack-C', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-B': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-B'], + }, + ], + }); + + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: cloudFormation, + }); + + // WHEN + const workflow = await listStacks( toolkit, { selectors: ['Test-Stack-A', 'Test-Stack-B', 'Test-Stack-C'] }); + + // THEN + expect(JSON.stringify(workflow)).toEqual(JSON.stringify([{ + id: 'Test-Stack-A', + name: 'Test-Stack-A', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [], + }, + { + id: 'Test-Stack-B', + name: 'Test-Stack-B', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [{ + id: 'Test-Stack-A', + dependencies: [], + }], + }, + { + id: 'Test-Stack-C', + name: 'Test-Stack-C', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [{ + id: 'Test-Stack-B', + dependencies: [{ + id: 'Test-Stack-A', + dependencies: [], + }], + }], + }])); + }); + + // In the context of stacks with cross-stack or cross-region references, + // the dependency mechanism is responsible for appropriately applying dependencies at the correct hierarchy level, + // typically at the top-level stacks. + // This involves handling the establishment of cross-references between stacks or nested stacks + // and generating assets for nested stack templates as necessary. + test('stacks with cross stack referencing', async () => { + let cloudExecutable = new MockCloudExecutable({ + stacks: [ + { + stackName: 'Test-Stack-A', + template: { + Resources: { + MyBucket1Reference: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'XXXXXXXXXXXXXXXXXXXXXXXXX', + Parameters: { + BucketName: { 'Fn::GetAtt': ['MyBucket1', 'Arn'] }, + }, + }, + }, + }, + }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-A': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-C'], + }, + MockStack.MOCK_STACK_C, + ], + }); + + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: cloudFormation, + }); + + // WHEN + const workflow = await listStacks( toolkit, { selectors: ['Test-Stack-A', 'Test-Stack-C'] }); + + // THEN + expect(JSON.stringify(workflow)).toEqual(JSON.stringify([{ + id: 'Test-Stack-C', + name: 'Test-Stack-C', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [], + }, + { + id: 'Test-Stack-A', + name: 'Test-Stack-A', + environment: { + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, + dependencies: [{ + id: 'Test-Stack-C', + dependencies: [], + }], + }])); + }); + + test('stacks with circular dependencies should error out', async () => { + let cloudExecutable = new MockCloudExecutable({ + stacks: [ + { + stackName: 'Test-Stack-A', + template: { Resources: { TemplateName: 'Test-Stack-A' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-A': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-B'], + }, + { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-B': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + depends: ['Test-Stack-A'], + }, + ], + }); + + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: cloudFormation, + }); + + // WHEN + await expect(() => + listStacks( toolkit, { selectors: ['Test-Stack-A', 'Test-Stack-B'] }), + ).rejects.toThrow('Could not determine ordering'); + }); +}); + +class MockStack { + public static readonly MOCK_STACK_A: TestStackArtifact = { + stackName: 'Test-Stack-A', + template: { Resources: { TemplateName: 'Test-Stack-A' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-A': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + }; + public static readonly MOCK_STACK_C: TestStackArtifact = { + stackName: 'Test-Stack-C', + template: { + Resources: { + MyBucket1: { + Type: 'AWS::S3::Bucket', + Properties: { + AccessControl: 'PublicRead', + }, + DeletionPolicy: 'Retain', + }, + }, + }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-C': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + }, + ], + }, + } +}