Skip to content

Commit e6205a6

Browse files
committed
Add describe change set lsp request
1 parent fdd89d8 commit e6205a6

File tree

8 files changed

+271
-10
lines changed

8 files changed

+271
-10
lines changed

src/handlers/StackHandler.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { parseIdentifiable } from '../protocol/LspParser';
66
import { Identifiable } from '../protocol/LspTypes';
77
import { ServerComponents } from '../server/ServerComponents';
88
import { analyzeCapabilities } from '../stacks/actions/CapabilityAnalyzer';
9+
import { mapChangesToStackChanges } from '../stacks/actions/StackActionOperations';
910
import {
1011
parseCreateDeploymentParams,
1112
parseDeleteChangeSetParams,
1213
parseListStackResourcesParams,
14+
parseDescribeChangeSetParams,
1315
parseStackActionParams,
1416
parseTemplateUriParams,
1517
parseGetStackEventsParams,
@@ -39,6 +41,8 @@ import {
3941
GetStackEventsParams,
4042
GetStackEventsResult,
4143
ClearStackEventsParams,
44+
DescribeChangeSetParams,
45+
DescribeChangeSetResult,
4246
} from '../stacks/StackRequestType';
4347
import { LoggerFactory } from '../telemetry/LoggerFactory';
4448
import { extractErrorMessage } from '../utils/Errors';
@@ -335,6 +339,31 @@ export function listStackResourcesHandler(
335339
};
336340
}
337341

342+
export function describeChangeSetHandler(
343+
components: ServerComponents,
344+
): RequestHandler<DescribeChangeSetParams, DescribeChangeSetResult, void> {
345+
return async (rawParams: DescribeChangeSetParams): Promise<DescribeChangeSetResult> => {
346+
log.debug({ Handler: 'describeChangeSetsHandler', rawParams });
347+
348+
const params = parseWithPrettyError(parseDescribeChangeSetParams, rawParams);
349+
350+
const result = await components.cfnService.describeChangeSet({
351+
ChangeSetName: params.changeSetName,
352+
IncludePropertyValues: true,
353+
StackName: params.stackName,
354+
});
355+
356+
return {
357+
changeSetName: params.changeSetName,
358+
stackName: params.stackName,
359+
status: result.Status ?? '',
360+
creationTime: result.CreationTime?.toISOString(),
361+
description: result.Description,
362+
changes: mapChangesToStackChanges(result.Changes),
363+
};
364+
};
365+
}
366+
338367
export function getStackEventsHandler(
339368
components: ServerComponents,
340369
): RequestHandler<GetStackEventsParams, GetStackEventsResult, void> {

src/protocol/LspStackHandlers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import {
4545
GetStackEventsRequest,
4646
ClearStackEventsParams,
4747
ClearStackEventsRequest,
48+
DescribeChangeSetParams,
49+
DescribeChangeSetResult,
50+
DescribeChangeSetRequest,
4851
} from '../stacks/StackRequestType';
4952
import { Identifiable } from './LspTypes';
5053

@@ -103,6 +106,10 @@ export class LspStackHandlers {
103106
this.connection.onRequest(ListChangeSetRequest.method, handler);
104107
}
105108

109+
onDescribeChangeSet(handler: RequestHandler<DescribeChangeSetParams, DescribeChangeSetResult, void>) {
110+
this.connection.onRequest(DescribeChangeSetRequest.method, handler);
111+
}
112+
106113
onDeleteChangeSet(handler: RequestHandler<DeleteChangeSetParams, CreateStackActionResult, void>) {
107114
this.connection.onRequest(DeleteChangeSetRequest.method, handler);
108115
}

src/server/CfnServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
describeChangeSetDeletionStatusHandler,
3939
getStackEventsHandler,
4040
clearStackEventsHandler,
41+
describeChangeSetHandler,
4142
} from '../handlers/StackHandler';
4243
import { LspComponents } from '../protocol/LspComponents';
4344
import { closeSafely } from '../utils/Closeable';
@@ -114,6 +115,7 @@ export class CfnServer {
114115
this.lsp.stackHandlers.onListStacks(listStacksHandler(this.components));
115116
this.lsp.stackHandlers.onListChangeSets(listChangeSetsHandler(this.components));
116117
this.lsp.stackHandlers.onListStackResources(listStackResourcesHandler(this.components));
118+
this.lsp.stackHandlers.onDescribeChangeSet(describeChangeSetHandler(this.components));
117119
this.lsp.stackHandlers.onGetStackTemplate(getManagedResourceStackTemplateHandler(this.components));
118120
this.lsp.stackHandlers.onGetStackEvents(getStackEventsHandler(this.components));
119121
this.lsp.stackHandlers.onClearStackEvents(clearStackEventsHandler(this.components));

src/stacks/StackRequestType.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StackSummary, StackStatus, StackResourceSummary, StackEvent } from '@aws-sdk/client-cloudformation';
22
import { RequestType } from 'vscode-languageserver-protocol';
3+
import { ChangeSetReference, StackChange } from './actions/StackActionRequestType';
34

45
export type ListStacksParams = {
56
statusToInclude?: StackStatus[];
@@ -33,16 +34,25 @@ export type ListChangeSetParams = {
3334
nextToken?: string;
3435
};
3536

37+
export type DescribeChangeSetParams = ChangeSetReference;
38+
39+
export type ChangeSetSummary = {
40+
changeSetName: string;
41+
status: string;
42+
creationTime?: string;
43+
description?: string;
44+
};
45+
3646
export type ListChangeSetResult = {
37-
changeSets: Array<{
38-
changeSetName: string;
39-
status: string;
40-
creationTime?: string;
41-
description?: string;
42-
}>;
47+
changeSets: Array<ChangeSetSummary>;
4348
nextToken?: string;
4449
};
4550

51+
export type DescribeChangeSetResult = ChangeSetSummary & {
52+
stackName: string;
53+
changes?: StackChange[];
54+
};
55+
4656
export const ListChangeSetRequest = new RequestType<ListChangeSetParams, ListChangeSetResult, void>(
4757
'aws/cfn/stack/changeSet/list',
4858
);
@@ -84,3 +94,7 @@ export type ClearStackEventsParams = {
8494
export const ClearStackEventsRequest = new RequestType<ClearStackEventsParams, void, void>(
8595
'aws/cfn/stack/events/clear',
8696
);
97+
98+
export const DescribeChangeSetRequest = new RequestType<DescribeChangeSetParams, DescribeChangeSetResult, void>(
99+
'aws/cfn/stack/changeSet/describe',
100+
);

src/stacks/actions/StackActionParser.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Capability } from '@aws-sdk/client-cloudformation';
22
import { z } from 'zod';
3-
import { ListStackResourcesParams, GetStackEventsParams, ClearStackEventsParams } from '../StackRequestType';
3+
import {
4+
ListStackResourcesParams,
5+
GetStackEventsParams,
6+
ClearStackEventsParams,
7+
DescribeChangeSetParams,
8+
} from '../StackRequestType';
49
import {
510
CreateDeploymentParams,
611
CreateValidationParams,
@@ -49,6 +54,11 @@ const DeleteChangeSetParamsSchema = z.object({
4954
changeSetName: z.string().min(1).max(128),
5055
});
5156

57+
const DescribeChangeSetParamsSchema = z.object({
58+
stackName: z.string().min(1).max(128),
59+
changeSetName: z.string().min(1).max(128),
60+
});
61+
5262
const TemplateUriSchema = z.string().min(1);
5363

5464
const ListStackResourcesParamsSchema = z.object({
@@ -94,3 +104,7 @@ export function parseGetStackEventsParams(input: unknown): GetStackEventsParams
94104
export function parseClearStackEventsParams(input: unknown): ClearStackEventsParams {
95105
return ClearStackEventsParamsSchema.parse(input);
96106
}
107+
108+
export function parseDescribeChangeSetParams(input: unknown): DescribeChangeSetParams {
109+
return DescribeChangeSetParamsSchema.parse(input);
110+
}

tst/unit/handlers/StackHandler.test.ts

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Capability, StackSummary, StackStatus, StackResourceSummary } from '@aws-sdk/client-cloudformation';
1+
import {
2+
Capability,
3+
StackSummary,
4+
StackStatus,
5+
StackResourceSummary,
6+
ChangeSetStatus,
7+
} from '@aws-sdk/client-cloudformation';
28
import { StubbedInstance } from 'ts-sinon';
39
import { describe, it, expect, vi, beforeEach } from 'vitest';
410
import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver';
@@ -22,8 +28,10 @@ import {
2228
deleteChangeSetHandler,
2329
describeChangeSetDeletionStatusHandler,
2430
getChangeSetDeletionStatusHandler,
31+
describeChangeSetHandler,
2532
} from '../../../src/handlers/StackHandler';
2633
import { analyzeCapabilities } from '../../../src/stacks/actions/CapabilityAnalyzer';
34+
import { mapChangesToStackChanges } from '../../../src/stacks/actions/StackActionOperations';
2735
import {
2836
TemplateUri,
2937
GetCapabilitiesResult,
@@ -32,7 +40,13 @@ import {
3240
StackActionPhase,
3341
StackActionState,
3442
} from '../../../src/stacks/actions/StackActionRequestType';
35-
import { ListStacksParams, ListStacksResult, ListStackResourcesResult } from '../../../src/stacks/StackRequestType';
43+
import {
44+
ListStacksParams,
45+
ListStacksResult,
46+
ListStackResourcesResult,
47+
DescribeChangeSetParams,
48+
DescribeChangeSetResult,
49+
} from '../../../src/stacks/StackRequestType';
3650
import {
3751
createMockComponents,
3852
createMockSyntaxTreeManager,
@@ -55,6 +69,7 @@ vi.mock('../../../src/stacks/actions/StackActionParser', () => ({
5569
parseCreateDeploymentParams: vi.fn((input) => input),
5670
parseDeleteChangeSetParams: vi.fn((input) => input),
5771
parseListStackResourcesParams: vi.fn((input) => input),
72+
parseDescribeChangeSetParams: vi.fn((input) => input),
5873
}));
5974

6075
vi.mock('../../../src/utils/ZodErrorWrapper', () => ({
@@ -65,6 +80,10 @@ vi.mock('../../../src/stacks/actions/CapabilityAnalyzer', () => ({
6580
analyzeCapabilities: vi.fn(),
6681
}));
6782

83+
vi.mock('../../../src/stacks/actions/StackActionOperations', () => ({
84+
mapChangesToStackChanges: vi.fn(),
85+
}));
86+
6887
describe('StackActionHandler', () => {
6988
let mockComponents: MockedServerComponents;
7089
let syntaxTreeManager: StubbedInstance<SyntaxTreeManager>;
@@ -690,4 +709,107 @@ describe('StackActionHandler', () => {
690709
expect(result.resources[0].logicalId).toBe('MyBucket');
691710
});
692711
});
712+
713+
describe('describeChangeSetHandler', () => {
714+
it('should return changeset details on success', async () => {
715+
const mockChangeSetResponse = {
716+
Status: ChangeSetStatus.CREATE_COMPLETE,
717+
CreationTime: new Date('2023-01-01T00:00:00Z'),
718+
Description: 'Test changeset',
719+
Changes: [
720+
{
721+
Action: 'Add',
722+
ResourceChange: {
723+
LogicalResourceId: 'MyBucket',
724+
ResourceType: 'AWS::S3::Bucket',
725+
},
726+
},
727+
],
728+
$metadata: {},
729+
};
730+
731+
const mockMappedChanges = [
732+
{
733+
type: 'Resource',
734+
resourceChange: {
735+
action: 'Add',
736+
logicalResourceId: 'MyBucket',
737+
resourceType: 'AWS::S3::Bucket',
738+
},
739+
},
740+
];
741+
742+
mockComponents.cfnService.describeChangeSet.resolves(mockChangeSetResponse);
743+
vi.mocked(mapChangesToStackChanges).mockReturnValue(mockMappedChanges);
744+
745+
const handler = describeChangeSetHandler(mockComponents);
746+
const params: DescribeChangeSetParams = {
747+
changeSetName: 'test-changeset',
748+
stackName: 'test-stack',
749+
};
750+
751+
const result = (await handler(params, {} as any)) as DescribeChangeSetResult;
752+
753+
expect(result).toEqual({
754+
changeSetName: 'test-changeset',
755+
stackName: 'test-stack',
756+
status: ChangeSetStatus.CREATE_COMPLETE,
757+
creationTime: '2023-01-01T00:00:00.000Z',
758+
description: 'Test changeset',
759+
changes: mockMappedChanges,
760+
});
761+
762+
expect(
763+
mockComponents.cfnService.describeChangeSet.calledWith({
764+
ChangeSetName: 'test-changeset',
765+
IncludePropertyValues: true,
766+
StackName: 'test-stack',
767+
}),
768+
).toBe(true);
769+
expect(mapChangesToStackChanges).toHaveBeenCalledWith(mockChangeSetResponse.Changes);
770+
});
771+
772+
it('should handle undefined optional fields', async () => {
773+
const mockChangeSetResponse = {
774+
Status: undefined,
775+
CreationTime: undefined,
776+
Description: undefined,
777+
Changes: undefined,
778+
$metadata: {},
779+
};
780+
781+
mockComponents.cfnService.describeChangeSet.resolves(mockChangeSetResponse);
782+
vi.mocked(mapChangesToStackChanges).mockReturnValue([]);
783+
784+
const handler = describeChangeSetHandler(mockComponents);
785+
const params: DescribeChangeSetParams = {
786+
changeSetName: 'test-changeset',
787+
stackName: 'test-stack',
788+
};
789+
790+
const result = (await handler(params, {} as any)) as DescribeChangeSetResult;
791+
792+
expect(result).toEqual({
793+
changeSetName: 'test-changeset',
794+
stackName: 'test-stack',
795+
status: '',
796+
creationTime: undefined,
797+
description: undefined,
798+
changes: [],
799+
});
800+
});
801+
802+
it('should propagate errors from cfnService', async () => {
803+
const error = new Error('ChangeSet not found');
804+
mockComponents.cfnService.describeChangeSet.rejects(error);
805+
806+
const handler = describeChangeSetHandler(mockComponents);
807+
const params: DescribeChangeSetParams = {
808+
changeSetName: 'non-existent-changeset',
809+
stackName: 'test-stack',
810+
};
811+
812+
await expect(handler(params, {} as any)).rejects.toThrow('ChangeSet not found');
813+
});
814+
});
693815
});

tst/unit/protocol/LspStackHandlers.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import {
3636
ListStackResourcesRequest,
3737
ListStackResourcesParams,
3838
ListStackResourcesResult,
39+
DescribeChangeSetParams,
40+
DescribeChangeSetResult,
41+
DescribeChangeSetRequest,
3942
} from '../../../src/stacks/StackRequestType';
4043

4144
describe('LspTemplateHandlers', () => {
@@ -150,4 +153,12 @@ describe('LspTemplateHandlers', () => {
150153

151154
expect(connection.onRequest.calledWith(ListStackResourcesRequest.method)).toBe(true);
152155
});
156+
157+
it('should register onDescribeChangeSet handler', () => {
158+
const mockHandler: RequestHandler<DescribeChangeSetParams, DescribeChangeSetResult, void> = vi.fn();
159+
160+
stackActionHandlers.onDescribeChangeSet(mockHandler);
161+
162+
expect(connection.onRequest.calledWith(DescribeChangeSetRequest.method)).toBe(true);
163+
});
153164
});

0 commit comments

Comments
 (0)