Skip to content

Commit 148bf7e

Browse files
authored
Add describe change set lsp request (#160)
1 parent 8ca42b8 commit 148bf7e

File tree

9 files changed

+259
-8
lines changed

9 files changed

+259
-8
lines changed

src/handlers/StackHandler.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ 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,
1314
parseCreateValidationParams,
15+
parseDescribeChangeSetParams,
1416
parseTemplateUriParams,
1517
parseGetStackEventsParams,
1618
parseClearStackEventsParams,
@@ -42,6 +44,8 @@ import {
4244
ClearStackEventsParams,
4345
GetStackOutputsParams,
4446
GetStackOutputsResult,
47+
DescribeChangeSetParams,
48+
DescribeChangeSetResult,
4549
} from '../stacks/StackRequestType';
4650
import { LoggerFactory } from '../telemetry/LoggerFactory';
4751
import { extractErrorMessage } from '../utils/Errors';
@@ -338,6 +342,29 @@ export function listStackResourcesHandler(
338342
};
339343
}
340344

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

src/protocol/LspStackHandlers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ import {
4848
GetStackOutputsParams,
4949
GetStackOutputsResult,
5050
GetStackOutputsRequest,
51+
DescribeChangeSetParams,
52+
DescribeChangeSetResult,
53+
DescribeChangeSetRequest,
5154
} from '../stacks/StackRequestType';
5255
import { Identifiable } from './LspTypes';
5356

@@ -106,6 +109,10 @@ export class LspStackHandlers {
106109
this.connection.onRequest(ListChangeSetRequest.method, handler);
107110
}
108111

112+
onDescribeChangeSet(handler: RequestHandler<DescribeChangeSetParams, DescribeChangeSetResult, void>) {
113+
this.connection.onRequest(DescribeChangeSetRequest.method, handler);
114+
}
115+
109116
onDeleteChangeSet(handler: RequestHandler<DeleteChangeSetParams, CreateStackActionResult, void>) {
110117
this.connection.onRequest(DeleteChangeSetRequest.method, handler);
111118
}

src/server/CfnServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getStackEventsHandler,
4040
clearStackEventsHandler,
4141
getStackOutputsHandler,
42+
describeChangeSetHandler,
4243
} from '../handlers/StackHandler';
4344
import { LspComponents } from '../protocol/LspComponents';
4445
import { closeSafely } from '../utils/Closeable';
@@ -115,6 +116,7 @@ export class CfnServer {
115116
this.lsp.stackHandlers.onListStacks(listStacksHandler(this.components));
116117
this.lsp.stackHandlers.onListChangeSets(listChangeSetsHandler(this.components));
117118
this.lsp.stackHandlers.onListStackResources(listStackResourcesHandler(this.components));
119+
this.lsp.stackHandlers.onDescribeChangeSet(describeChangeSetHandler(this.components));
118120
this.lsp.stackHandlers.onGetStackTemplate(getManagedResourceStackTemplateHandler(this.components));
119121
this.lsp.stackHandlers.onGetStackEvents(getStackEventsHandler(this.components));
120122
this.lsp.stackHandlers.onClearStackEvents(clearStackEventsHandler(this.components));

src/services/CfnService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export class CfnService {
135135
return await this.withClient((client) => client.send(new CreateChangeSetCommand(params)));
136136
}
137137

138+
@Measure({ name: 'describeChangeSet' })
138139
public async describeChangeSet(params: {
139140
ChangeSetName: string;
140141
IncludePropertyValues: boolean;

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, Output } 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
);
@@ -96,3 +106,7 @@ export type GetStackOutputsResult = {
96106
export const GetStackOutputsRequest = new RequestType<GetStackOutputsParams, GetStackOutputsResult, void>(
97107
'aws/cfn/stack/outputs',
98108
);
109+
110+
export const DescribeChangeSetRequest = new RequestType<DescribeChangeSetParams, DescribeChangeSetResult, void>(
111+
'aws/cfn/stack/changeSet/describe',
112+
);

src/stacks/actions/StackActionParser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
GetStackEventsParams,
66
ClearStackEventsParams,
77
GetStackOutputsParams,
8+
DescribeChangeSetParams,
89
} from '../StackRequestType';
910
import {
1011
CreateDeploymentParams,
@@ -65,6 +66,11 @@ const DeleteChangeSetParamsSchema = z.object({
6566
changeSetName: z.string().min(1).max(128),
6667
});
6768

69+
const DescribeChangeSetParamsSchema = z.object({
70+
stackName: z.string().min(1).max(128),
71+
changeSetName: z.string().min(1).max(128),
72+
});
73+
6874
const TemplateUriSchema = z.string().min(1);
6975

7076
const ListStackResourcesParamsSchema = z.object({
@@ -118,3 +124,7 @@ export function parseClearStackEventsParams(input: unknown): ClearStackEventsPar
118124
export function parseGetStackOutputsParams(input: unknown): GetStackOutputsParams {
119125
return GetStackOutputsParamsSchema.parse(input);
120126
}
127+
128+
export function parseDescribeChangeSetParams(input: unknown): DescribeChangeSetParams {
129+
return DescribeChangeSetParamsSchema.parse(input);
130+
}

tst/unit/handlers/StackHandler.test.ts

Lines changed: 118 additions & 1 deletion
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';
@@ -23,8 +29,10 @@ import {
2329
describeChangeSetDeletionStatusHandler,
2430
getChangeSetDeletionStatusHandler,
2531
getStackOutputsHandler,
32+
describeChangeSetHandler,
2633
} from '../../../src/handlers/StackHandler';
2734
import { analyzeCapabilities } from '../../../src/stacks/actions/CapabilityAnalyzer';
35+
import { mapChangesToStackChanges } from '../../../src/stacks/actions/StackActionOperations';
2836
import {
2937
TemplateUri,
3038
GetCapabilitiesResult,
@@ -38,6 +46,8 @@ import {
3846
ListStacksResult,
3947
ListStackResourcesResult,
4048
GetStackOutputsResult,
49+
DescribeChangeSetParams,
50+
DescribeChangeSetResult,
4151
} from '../../../src/stacks/StackRequestType';
4252
import {
4353
createMockComponents,
@@ -73,6 +83,10 @@ vi.mock('../../../src/stacks/actions/CapabilityAnalyzer', () => ({
7383
analyzeCapabilities: vi.fn(),
7484
}));
7585

86+
vi.mock('../../../src/stacks/actions/StackActionOperations', () => ({
87+
mapChangesToStackChanges: vi.fn(),
88+
}));
89+
7690
describe('StackActionHandler', () => {
7791
let mockComponents: MockedServerComponents;
7892
let syntaxTreeManager: StubbedInstance<SyntaxTreeManager>;
@@ -763,4 +777,107 @@ describe('StackActionHandler', () => {
763777
await expect(handler(params, {} as any)).rejects.toThrow(ResponseError);
764778
});
765779
});
780+
781+
describe('describeChangeSetHandler', () => {
782+
it('should return changeset details on success', async () => {
783+
const mockChangeSetResponse = {
784+
Status: ChangeSetStatus.CREATE_COMPLETE,
785+
CreationTime: new Date('2023-01-01T00:00:00Z'),
786+
Description: 'Test changeset',
787+
Changes: [
788+
{
789+
Action: 'Add',
790+
ResourceChange: {
791+
LogicalResourceId: 'MyBucket',
792+
ResourceType: 'AWS::S3::Bucket',
793+
},
794+
},
795+
],
796+
$metadata: {},
797+
};
798+
799+
const mockMappedChanges = [
800+
{
801+
type: 'Resource',
802+
resourceChange: {
803+
action: 'Add',
804+
logicalResourceId: 'MyBucket',
805+
resourceType: 'AWS::S3::Bucket',
806+
},
807+
},
808+
];
809+
810+
mockComponents.cfnService.describeChangeSet.resolves(mockChangeSetResponse);
811+
vi.mocked(mapChangesToStackChanges).mockReturnValue(mockMappedChanges);
812+
813+
const handler = describeChangeSetHandler(mockComponents);
814+
const params: DescribeChangeSetParams = {
815+
changeSetName: 'test-changeset',
816+
stackName: 'test-stack',
817+
};
818+
819+
const result = (await handler(params, {} as any)) as DescribeChangeSetResult;
820+
821+
expect(result).toEqual({
822+
changeSetName: 'test-changeset',
823+
stackName: 'test-stack',
824+
status: ChangeSetStatus.CREATE_COMPLETE,
825+
creationTime: '2023-01-01T00:00:00.000Z',
826+
description: 'Test changeset',
827+
changes: mockMappedChanges,
828+
});
829+
830+
expect(
831+
mockComponents.cfnService.describeChangeSet.calledWith({
832+
ChangeSetName: 'test-changeset',
833+
IncludePropertyValues: true,
834+
StackName: 'test-stack',
835+
}),
836+
).toBe(true);
837+
expect(mapChangesToStackChanges).toHaveBeenCalledWith(mockChangeSetResponse.Changes);
838+
});
839+
840+
it('should handle undefined optional fields', async () => {
841+
const mockChangeSetResponse = {
842+
Status: undefined,
843+
CreationTime: undefined,
844+
Description: undefined,
845+
Changes: undefined,
846+
$metadata: {},
847+
};
848+
849+
mockComponents.cfnService.describeChangeSet.resolves(mockChangeSetResponse);
850+
vi.mocked(mapChangesToStackChanges).mockReturnValue([]);
851+
852+
const handler = describeChangeSetHandler(mockComponents);
853+
const params: DescribeChangeSetParams = {
854+
changeSetName: 'test-changeset',
855+
stackName: 'test-stack',
856+
};
857+
858+
const result = (await handler(params, {} as any)) as DescribeChangeSetResult;
859+
860+
expect(result).toEqual({
861+
changeSetName: 'test-changeset',
862+
stackName: 'test-stack',
863+
status: '',
864+
creationTime: undefined,
865+
description: undefined,
866+
changes: [],
867+
});
868+
});
869+
870+
it('should propagate errors from cfnService', async () => {
871+
const error = new Error('ChangeSet not found');
872+
mockComponents.cfnService.describeChangeSet.rejects(error);
873+
874+
const handler = describeChangeSetHandler(mockComponents);
875+
const params: DescribeChangeSetParams = {
876+
changeSetName: 'non-existent-changeset',
877+
stackName: 'test-stack',
878+
};
879+
880+
await expect(handler(params, {} as any)).rejects.toThrow('ChangeSet not found');
881+
});
882+
});
766883
});

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)