Skip to content

Commit a7fac9d

Browse files
feat: list stack dependencies (#28995)
### Reason for this change Existing `cdk list` functionality does not display stack dependencies. This PR introduces that functionality. For instance, Existing functionality: ``` ❯ cdk list producer consumer ``` Feature functionality: ``` ❯ cdk list --show-dependencies - id: producer dependencies: [] - id: consumer dependencies: - id: producer dependencies: [] ``` ### Description of changes Changes are based on internal team design discussions. * A new flag `--show-dependencies` is being introduced for `list` cli command. * A new file `list-stacks.ts` is being added. * Adding `listStacks` function within the file for listing stack and their dependencies using cloud assembly from the cdk toolkit. ### Description of how you validated changes * Unit and Integration testing ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ### Co-Author Co-authored-by: @SankyRed ----- > NOTE: We are currently getting it reviewed by UX too so the final display output might change. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 4af0dfc commit a7fac9d

File tree

7 files changed

+754
-14
lines changed

7 files changed

+754
-14
lines changed

Diff for: packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

+57-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ if (process.env.PACKAGE_LAYOUT_VERSION === '1') {
2323
aws_sns: sns,
2424
aws_sqs: sqs,
2525
aws_lambda: lambda,
26-
aws_ecr_assets: docker
26+
aws_ecr_assets: docker,
27+
Stack
2728
} = require('aws-cdk-lib');
2829
}
2930

@@ -65,6 +66,59 @@ class YourStack extends cdk.Stack {
6566
}
6667
}
6768

69+
class ListMultipleDependentStack extends Stack {
70+
constructor(scope, id) {
71+
super(scope, id);
72+
73+
const dependentStack1 = new DependentStack1(this, 'DependentStack1');
74+
const dependentStack2 = new DependentStack2(this, 'DependentStack2');
75+
76+
this.addDependency(dependentStack1);
77+
this.addDependency(dependentStack2);
78+
}
79+
}
80+
81+
class DependentStack1 extends Stack {
82+
constructor(scope, id) {
83+
super(scope, id);
84+
85+
}
86+
}
87+
88+
class DependentStack2 extends Stack {
89+
constructor(scope, id) {
90+
super(scope, id);
91+
92+
}
93+
}
94+
95+
class ListStack extends Stack {
96+
constructor(scope, id) {
97+
super(scope, id);
98+
99+
const dependentStack = new DependentStack(this, 'DependentStack');
100+
101+
this.addDependency(dependentStack);
102+
}
103+
}
104+
105+
class DependentStack extends Stack {
106+
constructor(scope, id) {
107+
super(scope, id);
108+
109+
const innerDependentStack = new InnerDependentStack(this, 'InnerDependentStack');
110+
111+
this.addDependency(innerDependentStack);
112+
}
113+
}
114+
115+
class InnerDependentStack extends Stack {
116+
constructor(scope, id) {
117+
super(scope, id);
118+
119+
}
120+
}
121+
68122
class MigrateStack extends cdk.Stack {
69123
constructor(parent, id, props) {
70124
super(parent, id, props);
@@ -498,6 +552,8 @@ switch (stackSet) {
498552

499553
new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`);
500554
new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`);
555+
new ListStack(app, `${stackPrefix}-list-stacks`)
556+
new ListMultipleDependentStack(app, `${stackPrefix}-list-multiple-dependent-stacks`);
501557

502558
new YourStack(app, `${stackPrefix}-termination-protection`, {
503559
terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false,

Diff for: packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

+133
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,139 @@ integTest('cdk ls', withDefaultFixture(async (fixture) => {
844844
}
845845
}));
846846

847+
/**
848+
* Type to store stack dependencies recursively
849+
*/
850+
type DependencyDetails = {
851+
id: string;
852+
dependencies: DependencyDetails[];
853+
};
854+
855+
type StackDetails = {
856+
id: string;
857+
dependencies: DependencyDetails[];
858+
};
859+
860+
integTest('cdk ls --show-dependencies --json', withDefaultFixture(async (fixture) => {
861+
const listing = await fixture.cdk(['ls --show-dependencies --json'], { captureStderr: false });
862+
863+
const expectedStacks = [
864+
{
865+
id: 'test-1',
866+
dependencies: [],
867+
},
868+
{
869+
id: 'order-providing',
870+
dependencies: [],
871+
},
872+
{
873+
id: 'order-consuming',
874+
dependencies: [
875+
{
876+
id: 'order-providing',
877+
dependencies: [],
878+
},
879+
],
880+
},
881+
{
882+
id: 'with-nested-stack',
883+
dependencies: [],
884+
},
885+
{
886+
id: 'list-stacks',
887+
dependencies: [
888+
{
889+
id: 'liststacksDependentStack',
890+
dependencies: [
891+
{
892+
id: 'liststacksDependentStackInnerDependentStack',
893+
dependencies: [],
894+
},
895+
],
896+
},
897+
],
898+
},
899+
{
900+
id: 'list-multiple-dependent-stacks',
901+
dependencies: [
902+
{
903+
id: 'listmultipledependentstacksDependentStack1',
904+
dependencies: [],
905+
},
906+
{
907+
id: 'listmultipledependentstacksDependentStack2',
908+
dependencies: [],
909+
},
910+
],
911+
},
912+
];
913+
914+
function validateStackDependencies(stack: StackDetails) {
915+
expect(listing).toContain(stack.id);
916+
917+
function validateDependencies(dependencies: DependencyDetails[]) {
918+
for (const dependency of dependencies) {
919+
expect(listing).toContain(dependency.id);
920+
if (dependency.dependencies.length > 0) {
921+
validateDependencies(dependency.dependencies);
922+
}
923+
}
924+
}
925+
926+
if (stack.dependencies.length > 0) {
927+
validateDependencies(stack.dependencies);
928+
}
929+
}
930+
931+
for (const stack of expectedStacks) {
932+
validateStackDependencies(stack);
933+
}
934+
}));
935+
936+
integTest('cdk ls --show-dependencies --json --long', withDefaultFixture(async (fixture) => {
937+
const listing = await fixture.cdk(['ls --show-dependencies --json --long'], { captureStderr: false });
938+
939+
const expectedStacks = [
940+
{
941+
id: 'order-providing',
942+
name: 'order-providing',
943+
enviroment: {
944+
account: 'unknown-account',
945+
region: 'unknown-region',
946+
name: 'aws://unknown-account/unknown-region',
947+
},
948+
dependencies: [],
949+
},
950+
{
951+
id: 'order-consuming',
952+
name: 'order-consuming',
953+
enviroment: {
954+
account: 'unknown-account',
955+
region: 'unknown-region',
956+
name: 'aws://unknown-account/unknown-region',
957+
},
958+
dependencies: [
959+
{
960+
id: 'order-providing',
961+
dependencies: [],
962+
},
963+
],
964+
},
965+
];
966+
967+
for (const stack of expectedStacks) {
968+
expect(listing).toContain(fixture.fullStackName(stack.id));
969+
expect(listing).toContain(fixture.fullStackName(stack.name));
970+
expect(listing).toContain(stack.enviroment.account);
971+
expect(listing).toContain(stack.enviroment.name);
972+
expect(listing).toContain(stack.enviroment.region);
973+
for (const dependency of stack.dependencies) {
974+
expect(listing).toContain(fixture.fullStackName(dependency.id));
975+
}
976+
}
977+
978+
}));
979+
847980
integTest('synthing a stage with errors leads to failure', withDefaultFixture(async (fixture) => {
848981
const output = await fixture.cdk(['synth'], {
849982
allowErrExit: true,

Diff for: packages/aws-cdk/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The AWS CDK Toolkit provides the `cdk` command-line interface that can be used t
1515
| ------------------------------------- | ---------------------------------------------------------------------------------- |
1616
| [`cdk docs`](#cdk-docs) | Access the online documentation |
1717
| [`cdk init`](#cdk-init) | Start a new CDK project (app or library) |
18-
| [`cdk list`](#cdk-list) | List stacks in an application |
18+
| [`cdk list`](#cdk-list) | List stacks and their dependencies in an application |
1919
| [`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s) |
2020
| [`cdk diff`](#cdk-diff) | Diff stacks against current state |
2121
| [`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account |
@@ -74,7 +74,7 @@ $ cdk init lib --language=typescript
7474

7575
### `cdk list`
7676

77-
Lists the stacks modeled in the CDK app.
77+
Lists the stacks and their dependencies modeled in the CDK app.
7878

7979
```console
8080
$ # List all stacks in the CDK app 'node bin/main.js'

Diff for: packages/aws-cdk/lib/cdk-toolkit.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { StackActivityProgress } from './api/util/cloudformation/stack-activity-
2020
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, parseSourceOptions, generateTemplate, FromScan, TemplateSourceOptions, GenerateTemplateOutput, CfnTemplateGeneratorProvider, writeMigrateJsonFile, buildGenertedTemplateOutput, buildCfnClient, appendWarningsToReadme, isThereAWarning } from './commands/migrate';
2121
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
2222
import { ResourceImporter, removeNonImportResources } from './import';
23+
import { listStacks } from './list-stacks';
2324
import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging';
2425
import { deserializeStructure, serializeStructure } from './serialize';
2526
import { Configuration, PROJECT_CONFIG } from './settings';
@@ -613,16 +614,37 @@ export class CdkToolkit {
613614
}
614615
}
615616

616-
public async list(selectors: string[], options: { long?: boolean; json?: boolean } = { }): Promise<number> {
617-
const stacks = await this.selectStacksForList(selectors);
617+
public async list(selectors: string[], options: { long?: boolean; json?: boolean; showDeps?: boolean } = { }): Promise<number> {
618+
const stacks = await listStacks(this, {
619+
selectors: selectors,
620+
});
621+
622+
if (options.long && options.showDeps) {
623+
data(serializeStructure(stacks, options.json ?? false));
624+
return 0;
625+
}
626+
627+
if (options.showDeps) {
628+
const stackDeps = [];
629+
630+
for (const stack of stacks) {
631+
stackDeps.push({
632+
id: stack.id,
633+
dependencies: stack.dependencies,
634+
});
635+
}
636+
637+
data(serializeStructure(stackDeps, options.json ?? false));
638+
return 0;
639+
}
618640

619-
// if we are in "long" mode, emit the array as-is (JSON/YAML)
620641
if (options.long) {
621642
const long = [];
622-
for (const stack of stacks.stackArtifacts) {
643+
644+
for (const stack of stacks) {
623645
long.push({
624-
id: stack.hierarchicalId,
625-
name: stack.stackName,
646+
id: stack.id,
647+
name: stack.name,
626648
environment: stack.environment,
627649
});
628650
}
@@ -631,8 +653,8 @@ export class CdkToolkit {
631653
}
632654

633655
// just print stack IDs
634-
for (const stack of stacks.stackArtifacts) {
635-
data(stack.hierarchicalId);
656+
for (const stack of stacks) {
657+
data(stack.id);
636658
}
637659

638660
return 0; // exit-code
@@ -905,7 +927,7 @@ export class CdkToolkit {
905927
return assembly.stackById(stacks.firstStack.id);
906928
}
907929

908-
private assembly(cacheCloudAssembly?: boolean): Promise<CloudAssembly> {
930+
public assembly(cacheCloudAssembly?: boolean): Promise<CloudAssembly> {
909931
return this.props.cloudExecutable.synthesize(cacheCloudAssembly);
910932
}
911933

Diff for: packages/aws-cdk/lib/cli.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ async function parseCommandLineArguments(args: string[]) {
8787
.option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false })
8888
.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 })
8989
.command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs
90-
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }),
90+
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' })
91+
.option('show-dependencies', { type: 'boolean', default: false, alias: 'd', desc: 'Display stack dependency information for each stack' }),
9192
)
9293
.command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => yargs
9394
.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<n
498499

499500
case 'ls':
500501
case 'list':
501-
return cli.list(args.STACKS, { long: args.long, json: argv.json });
502+
return cli.list(args.STACKS, { long: args.long, json: argv.json, showDeps: args.showDependencies });
502503

503504
case 'diff':
504505
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL_CONTEXT);

0 commit comments

Comments
 (0)