Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/lib/payloads/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './logs-monitor';
export * from './hotswap';
export * from './gc';
export * from './import';
export * from './resource-details';
111 changes: 111 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/payloads/resource-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Details about a CloudFormation resource (without stackId, for use in nested output)
*/
export interface ResourceInfo {
/**
* The CloudFormation logical ID
*/
readonly logicalId: string;

/**
* The CloudFormation resource type (e.g., AWS::Lambda::Function)
*/
readonly type: string;

/**
* The CDK construct path (from aws:cdk:path metadata)
* Will be '<unknown>' if metadata is not available
*/
readonly constructPath: string;

/**
* Resources this resource depends on (from DependsOn)
*/
readonly dependsOn: string[];

/**
* Cross-stack imports (from Fn::ImportValue)
*/
readonly imports: string[];

/**
* The removal policy if specified
*/
readonly removalPolicy?: 'retain' | 'destroy' | 'snapshot';
}

/**
* Resources grouped by stack (for JSON output)
*/
export interface StackResources {
/**
* The stack ID
*/
readonly stackId: string;

/**
* Resources in this stack
*/
readonly resources: ResourceInfo[];
}

/**
* Details about a CloudFormation resource
*/
export interface ResourceDetails {
/**
* The stack containing this resource
*/
readonly stackId: string;

/**
* The CloudFormation logical ID
*/
readonly logicalId: string;

/**
* The CloudFormation resource type (e.g., AWS::Lambda::Function)
*/
readonly type: string;

/**
* The CDK construct path (from aws:cdk:path metadata)
* Will be '<unknown>' if metadata is not available
*/
readonly constructPath: string;

/**
* Resources this resource depends on (from DependsOn)
*/
readonly dependsOn: string[];

/**
* Cross-stack imports (from Fn::ImportValue)
*/
readonly imports: string[];

/**
* The removal policy if specified
*/
readonly removalPolicy?: 'retain' | 'destroy' | 'snapshot';
}

/**
* Extended details for --explain output
*/
export interface ResourceExplainDetails extends ResourceDetails {
/**
* The Condition attached to this resource, if any
*/
readonly condition?: string;

/**
* Update policy if specified
*/
readonly updatePolicy?: string;

/**
* Creation policy if specified
*/
readonly creationPolicy?: string;
}
139 changes: 139 additions & 0 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type { Deployments, SuccessfulDeployStackResult } from '../api/deployment
import { mappingsByEnvironment, parseMappingGroups } from '../api/refactor';
import { type Tag } from '../api/tags';
import { StackActivityProgress } from '../commands/deploy';
import { listResources, explainResource } from '../commands/list-resources';
import { listStacks } from '../commands/list-stacks';
import type { FromScan, GenerateTemplateOutput } from '../commands/migrate';
import {
Expand Down Expand Up @@ -1044,6 +1045,143 @@ export class CdkToolkit {
return 0; // exit-code
}

/**
* List all resources in the specified stack(s)
*/
public async resources(
selectors: string[],
options: { json?: boolean; long?: boolean; all?: boolean; type?: string; explain?: string; ignoreCase?: boolean } = {},
): Promise<number> {
const io = this.ioHost.asIoHelper();

// Handle --explain mode (requires single stack)
if (options.explain) {
const resource = await explainResource(this, {
selectors,
logicalId: options.explain,
ignoreCase: options.ignoreCase,
});

if (!resource) {
throw new ToolkitError(`Resource '${options.explain}' not found`);
}

await printSerializedObject(io, resource, options.json ?? false);
return 0;
}

// List all resources (with optional type filter)
const resources = await listResources(this, { selectors, type: options.type, all: options.all, ignoreCase: options.ignoreCase });

if (resources.length === 0) {
if (options.type) {
await io.defaults.info(`No resources of type '${options.type}' found`);
} else {
await io.defaults.info('No resources found');
}
return 0;
}

if (options.json) {
// Group resources by stack for cleaner JSON output
const byStack = new Map<string, typeof resources>();
for (const r of resources) {
const list = byStack.get(r.stackId) ?? [];
list.push(r);
byStack.set(r.stackId, list);
}

const groupedOutput = [...byStack.entries()].map(([stackId, stackResources]) => ({
stackId,
resources: stackResources.map(({ stackId: _, ...rest }) => rest),
}));

await printSerializedObject(io, groupedOutput, true);
return 0;
}

// Group resources by type
const byType = new Map<string, typeof resources>();
for (const r of resources) {
const list = byType.get(r.type) ?? [];
list.push(r);
byType.set(r.type, list);
}

// Sort types by count (descending), then alphabetically
const sortedTypes = [...byType.entries()].sort((a, b) => {
const countDiff = b[1].length - a[1].length;
if (countDiff !== 0) return countDiff;
return a[0].localeCompare(b[0]);
});

// Show detailed grouped output if --long or --type filter is used
if (options.long || options.type) {
const lines: string[] = [];

for (const [type, typeResources] of sortedTypes) {
lines.push('');
lines.push(chalk.bold.cyan(`${type} (${typeResources.length})`));

for (const r of typeResources) {
lines.push(` ${chalk.white(r.logicalId.padEnd(40))} ${chalk.gray(r.constructPath)}`);
}
}

lines.push('');
lines.push(`${resources.length} resource(s) total`);

await io.defaults.result(lines.join('\n'));
return 0;
}

// Default: Summary mode - show counts by type, grouped by stack
const byStack = new Map<string, typeof resources>();
for (const r of resources) {
const list = byStack.get(r.stackId) ?? [];
list.push(r);
byStack.set(r.stackId, list);
}

const lines: string[] = [];

for (const [stackId, stackResources] of byStack) {
if (lines.length > 0) {
lines.push(''); // Blank line between stacks
}

lines.push(`${chalk.bold(stackId)}: ${stackResources.length} resources`);
lines.push('');

// Group by type within this stack
const stackByType = new Map<string, typeof stackResources>();
for (const r of stackResources) {
const list = stackByType.get(r.type) ?? [];
list.push(r);
stackByType.set(r.type, list);
}

// Sort types by count (descending), then alphabetically
const stackSortedTypes = [...stackByType.entries()].sort((a, b) => {
const countDiff = b[1].length - a[1].length;
if (countDiff !== 0) return countDiff;
return a[0].localeCompare(b[0]);
});

for (const [type, typeResources] of stackSortedTypes) {
const shortType = type.replace(/^AWS::/, '');
const dots = '.'.repeat(Math.max(1, 45 - shortType.length));
lines.push(`${shortType} ${chalk.gray(dots)} ${typeResources.length}`);
}
}

lines.push('');
lines.push(chalk.gray(`Use ${chalk.white('--long')} for full list, ${chalk.white('--type <TYPE>')} to filter, ${chalk.white('--all')} to include permissions`));

await io.defaults.result(lines.join('\n'));
return 0;
}

/**
* Synthesize the given set of stacks (called when the user runs 'cdk synth')
*
Expand Down Expand Up @@ -2175,3 +2313,4 @@ function requiresApproval(requireApproval: RequireApproval, permissionChangeType
return requireApproval === RequireApproval.ANYCHANGE ||
requireApproval === RequireApproval.BROADENING && permissionChangeType === PermissionChangeType.BROADENING;
}

38 changes: 38 additions & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,44 @@ export async function makeConfig(): Promise<CliConfig> {
'show-dependencies': { type: 'boolean', default: false, alias: 'd', desc: 'Display stack dependency information for each stack' },
},
},
'resources': {
arg: {
name: 'STACKS',
variadic: true,
},
description: 'Lists all CloudFormation resources in the specified stack(s)',
options: {
'long': {
type: 'boolean',
alias: 'l',
default: false,
desc: 'Display full resource list grouped by type (default shows summary counts)',
},
'all': {
type: 'boolean',
alias: 'a',
default: false,
desc: 'Include all resources (by default, noisy types like Lambda::Permission are hidden)',
},
'type': {
type: 'string',
alias: 't',
desc: 'Filter resources by type (case-insensitive partial match, e.g., "lambda" or "dynamodb")',
requiresArg: true,
},
'explain': {
type: 'string',
desc: 'Show detailed information for a specific resource by logical ID',
requiresArg: true,
},
'ignore-case': {
type: 'boolean',
alias: 'i',
default: false,
desc: 'Use case-insensitive matching for stack name patterns',
},
},
},
'synth': {
arg: {
name: 'STACKS',
Expand Down
38 changes: 38 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,44 @@
}
}
},
"resources": {
"arg": {
"name": "STACKS",
"variadic": true
},
"description": "Lists all CloudFormation resources in the specified stack(s)",
"options": {
"long": {
"type": "boolean",
"alias": "l",
"default": false,
"desc": "Display full resource list grouped by type (default shows summary counts)"
},
"all": {
"type": "boolean",
"alias": "a",
"default": false,
"desc": "Include all resources (by default, noisy types like Lambda::Permission are hidden)"
},
"type": {
"type": "string",
"alias": "t",
"desc": "Filter resources by type (case-insensitive partial match, e.g., \"lambda\" or \"dynamodb\")",
"requiresArg": true
},
"explain": {
"type": "string",
"desc": "Show detailed information for a specific resource by logical ID",
"requiresArg": true
},
"ignore-case": {
"type": "boolean",
"alias": "i",
"default": false,
"desc": "Use case-insensitive matching for stack name patterns"
}
}
},
"synth": {
"arg": {
"name": "STACKS",
Expand Down
11 changes: 11 additions & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
showDeps: args.showDependencies,
});

case 'resources':
ioHost.currentAction = 'resources';
return cli.resources(args.STACKS, {
json: argv.json,
long: args.long,
all: args.all,
type: args.type,
explain: args.explain,
ignoreCase: args.ignoreCase,
});

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