Skip to content

Commit

Permalink
feat: list stack dependencies (#28995)
Browse files Browse the repository at this point in the history
### 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*
  • Loading branch information
vinayak-kukreja authored Mar 5, 2024
1 parent 4af0dfc commit a7fac9d
Show file tree
Hide file tree
Showing 7 changed files with 754 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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'
Expand Down
40 changes: 31 additions & 9 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -613,16 +614,37 @@ export class CdkToolkit {
}
}

public async list(selectors: string[], options: { long?: boolean; json?: boolean } = { }): Promise<number> {
const stacks = await this.selectStacksForList(selectors);
public async list(selectors: string[], options: { long?: boolean; json?: boolean; showDeps?: boolean } = { }): Promise<number> {
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,
});
}
Expand All @@ -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
Expand Down Expand Up @@ -905,7 +927,7 @@ export class CdkToolkit {
return assembly.stackById(stacks.firstStack.id);
}

private assembly(cacheCloudAssembly?: boolean): Promise<CloudAssembly> {
public assembly(cacheCloudAssembly?: boolean): Promise<CloudAssembly> {
return this.props.cloudExecutable.synthesize(cacheCloudAssembly);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down Expand Up @@ -498,7 +499,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n

case 'ls':
case 'list':
return cli.list(args.STACKS, { long: args.long, json: argv.json });
return cli.list(args.STACKS, { long: args.long, json: argv.json, showDeps: args.showDependencies });

case 'diff':
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL_CONTEXT);
Expand Down
Loading

0 comments on commit a7fac9d

Please sign in to comment.