Skip to content

Commit bcc357d

Browse files
rongyamazonkddejong
authored andcommitted
Added new command for listStackResources
1 parent 27f9c65 commit bcc357d

File tree

10 files changed

+209
-9
lines changed

10 files changed

+209
-9
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/handlers/StackHandler.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
ListStacksResult,
3232
ListChangeSetParams,
3333
ListChangeSetResult,
34+
ListStackResourcesParams,
35+
ListStackResourcesResult,
3436
} from '../stacks/StackRequestType';
3537
import { LoggerFactory } from '../telemetry/LoggerFactory';
3638
import { extractErrorMessage } from '../utils/Errors';
@@ -330,6 +332,20 @@ export function listChangeSetsHandler(
330332
};
331333
}
332334

335+
export function listStackResourcesHandler(
336+
components: ServerComponents,
337+
): RequestHandler<ListStackResourcesParams, ListStackResourcesResult, void> {
338+
return async (params: ListStackResourcesParams): Promise<ListStackResourcesResult> => {
339+
try {
340+
const response = await components.cfnService.listStackResources({ StackName: params.stackName });
341+
return { resources: response.StackResourceSummaries ?? [] };
342+
} catch (error) {
343+
log.error({ error: extractErrorMessage(error) }, 'Error listing stack resources');
344+
return { resources: [] };
345+
}
346+
};
347+
}
348+
333349
function handleStackActionError(error: unknown, contextMessage: string): never {
334350
if (error instanceof ResponseError) {
335351
throw error;

src/protocol/LspStackHandlers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import {
3131
ListStacksParams,
3232
ListStacksResult,
3333
ListStacksRequest,
34+
ListStackResourcesParams,
35+
ListStackResourcesResult,
36+
ListStackResourcesRequest,
3437
GetStackTemplateParams,
3538
GetStackTemplateResult,
3639
GetStackTemplateRequest,
@@ -83,6 +86,10 @@ export class LspStackHandlers {
8386
this.connection.onRequest(ListStacksRequest.method, handler);
8487
}
8588

89+
onListStackResources(handler: RequestHandler<ListStackResourcesParams, ListStackResourcesResult, void>) {
90+
this.connection.onRequest(ListStackResourcesRequest.method, handler);
91+
}
92+
8693
onGetStackTemplate(handler: RequestHandler<GetStackTemplateParams, GetStackTemplateResult | undefined, void>) {
8794
this.connection.onRequest(GetStackTemplateRequest.method, handler);
8895
}

src/server/CfnServer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import {
2828
} from '../handlers/ResourceHandler';
2929
import {
3030
listStacksHandler,
31+
<<<<<<< HEAD
3132
listChangeSetsHandler,
33+
=======
34+
listStackResourcesHandler,
35+
>>>>>>> 29939f4 (Added new command for listStackResources)
3236
createValidationHandler,
3337
createDeploymentHandler,
3438
getValidationStatusHandler,
@@ -119,6 +123,7 @@ export class CfnServer {
119123
);
120124
this.lsp.stackHandlers.onListStacks(listStacksHandler(this.components));
121125
this.lsp.stackHandlers.onListChangeSets(listChangeSetsHandler(this.components));
126+
this.lsp.stackHandlers.onListStackResources(listStackResourcesHandler(this.components));
122127
this.lsp.stackHandlers.onGetStackTemplate(getManagedResourceStackTemplateHandler(this.components));
123128

124129
this.lsp.resourceHandlers.onListResources(listResourcesHandler(this.components));

src/services/CfnService.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
TypeSummary,
4343
DescribeTypeOutput,
4444
StackSummary,
45+
StackResourceSummary,
4546
StackStatus,
4647
waitUntilChangeSetCreateComplete,
4748
waitUntilStackUpdateComplete,
@@ -196,11 +197,33 @@ export class CfnService {
196197
return await this.withClient((client) => client.send(new DescribeStackResourceCommand(params)));
197198
}
198199

199-
public async listStackResources(params: {
200-
StackName: string;
201-
NextToken?: string;
202-
}): Promise<ListStackResourcesCommandOutput> {
203-
return await this.withClient((client) => client.send(new ListStackResourcesCommand(params)));
200+
public async listStackResources(params: { StackName: string }): Promise<ListStackResourcesCommandOutput> {
201+
return await this.withClient(async (client) => {
202+
const allResources: StackResourceSummary[] = [];
203+
let nextToken: string | undefined;
204+
let lastResponse: ListStackResourcesCommandOutput;
205+
206+
do {
207+
lastResponse = await client.send(
208+
new ListStackResourcesCommand({
209+
StackName: params.StackName,
210+
NextToken: nextToken,
211+
}),
212+
);
213+
214+
if (lastResponse.StackResourceSummaries) {
215+
allResources.push(...lastResponse.StackResourceSummaries);
216+
}
217+
218+
nextToken = lastResponse.NextToken;
219+
} while (nextToken);
220+
221+
return {
222+
...lastResponse,
223+
StackResourceSummaries: allResources,
224+
NextToken: undefined,
225+
};
226+
});
204227
}
205228

206229
public async describeStackResourceDrifts(params: {

src/stacks/StackRequestType.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StackSummary, StackStatus } from '@aws-sdk/client-cloudformation';
1+
import { StackSummary, StackStatus, StackResourceSummary } from '@aws-sdk/client-cloudformation';
22
import { RequestType } from 'vscode-languageserver-protocol';
33

44
export type ListStacksParams = {
@@ -46,3 +46,15 @@ export type ListChangeSetResult = {
4646
export const ListChangeSetRequest = new RequestType<ListChangeSetParams, ListChangeSetResult, void>(
4747
'aws/cfn/stack/changeSet/list',
4848
);
49+
50+
export type ListStackResourcesParams = {
51+
stackName: string;
52+
};
53+
54+
export type ListStackResourcesResult = {
55+
resources: StackResourceSummary[];
56+
};
57+
58+
export const ListStackResourcesRequest = new RequestType<ListStackResourcesParams, ListStackResourcesResult, void>(
59+
'aws/cfn/stack/resources',
60+
);

tst/unit/handlers/StackHandler.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Capability, StackSummary, StackStatus } from '@aws-sdk/client-cloudformation';
1+
import { Capability, StackSummary, StackStatus, StackResourceSummary } from '@aws-sdk/client-cloudformation';
22
import { StubbedInstance } from 'ts-sinon';
33
import { describe, it, expect, vi, beforeEach } from 'vitest';
44
import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver';
@@ -15,6 +15,7 @@ import {
1515
getValidationStatusHandler,
1616
getDeploymentStatusHandler,
1717
listStacksHandler,
18+
listStackResourcesHandler,
1819
describeValidationStatusHandler,
1920
describeDeploymentStatusHandler,
2021
getTemplateResourcesHandler,
@@ -31,7 +32,12 @@ import {
3132
StackActionPhase,
3233
StackActionState,
3334
} from '../../../src/stacks/actions/StackActionRequestType';
34-
import { ListStacksParams, ListStacksResult } from '../../../src/stacks/StackRequestType';
35+
import {
36+
ListStacksParams,
37+
ListStacksResult,
38+
ListStackResourcesParams,
39+
ListStackResourcesResult,
40+
} from '../../../src/stacks/StackRequestType';
3541
import {
3642
createMockComponents,
3743
createMockSyntaxTreeManager,
@@ -453,6 +459,50 @@ describe('StackActionHandler', () => {
453459
});
454460
});
455461

462+
describe('listStackResourcesHandler', () => {
463+
it('should return resources on success', async () => {
464+
const mockResources: StackResourceSummary[] = [
465+
{
466+
LogicalResourceId: 'MyBucket',
467+
ResourceType: 'AWS::S3::Bucket',
468+
ResourceStatus: 'CREATE_COMPLETE',
469+
} as StackResourceSummary,
470+
];
471+
472+
mockComponents.cfnService.listStackResources.resolves({
473+
StackResourceSummaries: mockResources,
474+
$metadata: {},
475+
});
476+
477+
const handler = listStackResourcesHandler(mockComponents);
478+
const params: ListStackResourcesParams = { stackName: 'test-stack' };
479+
const result = (await handler(params, {} as any)) as ListStackResourcesResult;
480+
481+
expect(result.resources).toEqual(mockResources);
482+
expect(mockComponents.cfnService.listStackResources.calledWith({ StackName: 'test-stack' })).toBe(true);
483+
});
484+
485+
it('should return empty array on error', async () => {
486+
mockComponents.cfnService.listStackResources.rejects(new Error('API Error'));
487+
488+
const handler = listStackResourcesHandler(mockComponents);
489+
const params: ListStackResourcesParams = { stackName: 'test-stack' };
490+
const result = (await handler(params, {} as any)) as ListStackResourcesResult;
491+
492+
expect(result.resources).toEqual([]);
493+
});
494+
495+
it('should handle undefined StackResourceSummaries', async () => {
496+
mockComponents.cfnService.listStackResources.resolves({ StackResourceSummaries: undefined, $metadata: {} });
497+
498+
const handler = listStackResourcesHandler(mockComponents);
499+
const params: ListStackResourcesParams = { stackName: 'test-stack' };
500+
const result = (await handler(params, {} as any)) as ListStackResourcesResult;
501+
502+
expect(result.resources).toEqual([]);
503+
});
504+
});
505+
456506
describe('getTemplateResourcesHandler', () => {
457507
it('returns empty array when no syntax tree found', () => {
458508
const templateUri: TemplateUri = 'test://template.yaml';

tst/unit/protocol/LspStackHandlers.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ import {
2929
DeleteChangeSetParams,
3030
DescribeDeletionStatusResult,
3131
} from '../../../src/stacks/actions/StackActionRequestType';
32+
import {
33+
ListStacksRequest,
34+
ListStacksParams,
35+
ListStacksResult,
36+
ListStackResourcesRequest,
37+
ListStackResourcesParams,
38+
ListStackResourcesResult,
39+
} from '../../../src/stacks/StackRequestType';
3240

3341
describe('LspTemplateHandlers', () => {
3442
let connection: StubbedInstance<Connection>;
@@ -126,4 +134,20 @@ describe('LspTemplateHandlers', () => {
126134

127135
expect(connection.onRequest.calledWith(DescribeChangeSetDeletionStatusRequest.method)).toBe(true);
128136
});
137+
138+
it('should register onListStacks handler', () => {
139+
const mockHandler: RequestHandler<ListStacksParams, ListStacksResult, void> = vi.fn();
140+
141+
stackActionHandlers.onListStacks(mockHandler);
142+
143+
expect(connection.onRequest.calledWith(ListStacksRequest.method)).toBe(true);
144+
});
145+
146+
it('should register onListStackResources handler', () => {
147+
const mockHandler: RequestHandler<ListStackResourcesParams, ListStackResourcesResult, void> = vi.fn();
148+
149+
stackActionHandlers.onListStackResources(mockHandler);
150+
151+
expect(connection.onRequest.calledWith(ListStackResourcesRequest.method)).toBe(true);
152+
});
129153
});

tst/unit/server/CfnServer.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe('CfnServer', () => {
4848
expect(mockFeatures.stackHandlers.onGetValidationStatus.calledOnce).toBe(true);
4949
expect(mockFeatures.stackHandlers.onGetDeploymentStatus.calledOnce).toBe(true);
5050
expect(mockFeatures.stackHandlers.onListStacks.calledOnce).toBe(true);
51+
expect(mockFeatures.stackHandlers.onListStackResources.calledOnce).toBe(true);
5152
});
5253
});
5354

tst/unit/services/CfnService.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,68 @@ describe('CfnService', () => {
498498
expect(result).toEqual(MOCK_RESPONSES.LIST_STACK_RESOURCES);
499499
});
500500

501+
it('should handle pagination and collect all resources', async () => {
502+
const page1 = {
503+
...MOCK_RESPONSES.LIST_STACK_RESOURCES,
504+
StackResourceSummaries: [
505+
{
506+
LogicalResourceId: 'Resource1',
507+
ResourceType: 'AWS::S3::Bucket',
508+
LastUpdatedTimestamp: new Date(),
509+
ResourceStatus: 'CREATE_COMPLETE' as const,
510+
},
511+
{
512+
LogicalResourceId: 'Resource2',
513+
ResourceType: 'AWS::S3::Bucket',
514+
LastUpdatedTimestamp: new Date(),
515+
ResourceStatus: 'CREATE_COMPLETE' as const,
516+
},
517+
],
518+
NextToken: 'token1',
519+
};
520+
const page2 = {
521+
...MOCK_RESPONSES.LIST_STACK_RESOURCES,
522+
StackResourceSummaries: [
523+
{
524+
LogicalResourceId: 'Resource3',
525+
ResourceType: 'AWS::S3::Bucket',
526+
LastUpdatedTimestamp: new Date(),
527+
ResourceStatus: 'CREATE_COMPLETE' as const,
528+
},
529+
],
530+
NextToken: undefined,
531+
};
532+
533+
cloudFormationMock.on(ListStackResourcesCommand).resolvesOnce(page1).resolvesOnce(page2);
534+
535+
const result = await service.listStackResources({
536+
StackName: TEST_CONSTANTS.STACK_NAME,
537+
});
538+
539+
expect(result.StackResourceSummaries).toHaveLength(3);
540+
expect(result.StackResourceSummaries).toEqual([
541+
{
542+
LogicalResourceId: 'Resource1',
543+
ResourceType: 'AWS::S3::Bucket',
544+
LastUpdatedTimestamp: expect.any(Date),
545+
ResourceStatus: 'CREATE_COMPLETE' as const,
546+
},
547+
{
548+
LogicalResourceId: 'Resource2',
549+
ResourceType: 'AWS::S3::Bucket',
550+
LastUpdatedTimestamp: expect.any(Date),
551+
ResourceStatus: 'CREATE_COMPLETE' as const,
552+
},
553+
{
554+
LogicalResourceId: 'Resource3',
555+
ResourceType: 'AWS::S3::Bucket',
556+
LastUpdatedTimestamp: expect.any(Date),
557+
ResourceStatus: 'CREATE_COMPLETE' as const,
558+
},
559+
]);
560+
expect(result.NextToken).toBeUndefined();
561+
});
562+
501563
it('should throw StackNotFoundException when API call fails', async () => {
502564
const error = createStackNotFoundError();
503565
cloudFormationMock.on(ListStackResourcesCommand).rejects(error);

0 commit comments

Comments
 (0)