Skip to content

Commit 205779c

Browse files
authored
Support DescribeEvents as LSP method (#358)
1 parent d1465d6 commit 205779c

File tree

7 files changed

+204
-1
lines changed

7 files changed

+204
-1
lines changed

src/handlers/StackHandler.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
parseGetStackEventsParams,
1919
parseClearStackEventsParams,
2020
parseDescribeStackParams,
21+
parseDescribeEventsParams,
2122
} from '../stacks/actions/StackActionParser';
2223
import {
2324
TemplateUri,
@@ -48,6 +49,8 @@ import {
4849
DescribeStackResult,
4950
DescribeChangeSetParams,
5051
DescribeChangeSetResult,
52+
DescribeEventsParams,
53+
DescribeEventsResult,
5154
} from '../stacks/StackRequestType';
5255
import { TelemetryService } from '../telemetry/TelemetryService';
5356
import { EventType } from '../usageTracker/UsageTracker';
@@ -444,3 +447,28 @@ export function describeStackHandler(
444447
}
445448
};
446449
}
450+
451+
export function describeEventsHandler(
452+
components: ServerComponents,
453+
): RequestHandler<DescribeEventsParams, DescribeEventsResult, void> {
454+
return async (rawParams): Promise<DescribeEventsResult> => {
455+
try {
456+
const params = parseWithPrettyError(parseDescribeEventsParams, rawParams);
457+
458+
const response = await components.cfnService.describeEvents({
459+
StackName: params.stackName,
460+
ChangeSetName: params.changeSetName,
461+
OperationId: params.operationId,
462+
FailedEventsOnly: params.failedEventsOnly,
463+
NextToken: params.nextToken,
464+
});
465+
466+
return {
467+
events: response.OperationEvents ?? [],
468+
nextToken: response.NextToken,
469+
};
470+
} catch (error) {
471+
handleLspError(error, 'Failed to describe events');
472+
}
473+
};
474+
}

src/protocol/LspStackHandlers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ import {
5353
DescribeChangeSetParams,
5454
DescribeChangeSetResult,
5555
DescribeChangeSetRequest,
56+
DescribeEventsParams,
57+
DescribeEventsResult,
58+
DescribeEventsRequest,
5659
} from '../stacks/StackRequestType';
5760
import { Identifiable } from './LspTypes';
5861

@@ -142,4 +145,8 @@ export class LspStackHandlers {
142145
onDescribeStack(handler: RequestHandler<DescribeStackParams, DescribeStackResult, void>) {
143146
this.connection.onRequest(DescribeStackRequest.method, handler);
144147
}
148+
149+
onDescribeEvents(handler: RequestHandler<DescribeEventsParams, DescribeEventsResult, void>) {
150+
this.connection.onRequest(DescribeEventsRequest.method, handler);
151+
}
145152
}

src/server/CfnServer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
clearStackEventsHandler,
4949
describeStackHandler,
5050
describeChangeSetHandler,
51+
describeEventsHandler,
5152
} from '../handlers/StackHandler';
5253
import { LspComponents } from '../protocol/LspComponents';
5354
import { LoggerFactory } from '../telemetry/LoggerFactory';
@@ -228,6 +229,12 @@ export class CfnServer {
228229
withOnlineGuard(this.components.onlineFeatureGuard, describeStackHandler(this.components)),
229230
),
230231
);
232+
this.lsp.stackHandlers.onDescribeEvents(
233+
withTelemetryContext(
234+
'Stack.Describe.Events',
235+
withOnlineGuard(this.components.onlineFeatureGuard, describeEventsHandler(this.components)),
236+
),
237+
);
231238

232239
this.lsp.cfnEnvironmentHandlers.onParseCfnEnvironmentFiles(
233240
withTelemetryContext('Cfn.Environment.Parse', parseCfnEnvironmentFilesHandler()),

src/stacks/StackRequestType.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { StackSummary, StackStatus, StackResourceSummary, StackEvent, Stack } from '@aws-sdk/client-cloudformation';
1+
import {
2+
StackSummary,
3+
StackStatus,
4+
StackResourceSummary,
5+
StackEvent,
6+
Stack,
7+
OperationEvent,
8+
} from '@aws-sdk/client-cloudformation';
29
import { RequestType } from 'vscode-languageserver-protocol';
310
import { ChangeSetReference, DeploymentMode, StackChange } from './actions/StackActionRequestType';
411

@@ -111,3 +118,20 @@ export const DescribeStackRequest = new RequestType<DescribeStackParams, Describ
111118
export const DescribeChangeSetRequest = new RequestType<DescribeChangeSetParams, DescribeChangeSetResult, void>(
112119
'aws/cfn/stack/changeSet/describe',
113120
);
121+
122+
export type DescribeEventsParams = {
123+
stackName?: string;
124+
changeSetName?: string;
125+
operationId?: string;
126+
failedEventsOnly?: boolean;
127+
nextToken?: string;
128+
};
129+
130+
export type DescribeEventsResult = {
131+
events: OperationEvent[];
132+
nextToken?: string;
133+
};
134+
135+
export const DescribeEventsRequest = new RequestType<DescribeEventsParams, DescribeEventsResult, void>(
136+
'aws/cfn/stack/events/describe',
137+
);

src/stacks/actions/StackActionParser.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ClearStackEventsParams,
88
DescribeStackParams,
99
DescribeChangeSetParams,
10+
DescribeEventsParams,
1011
} from '../StackRequestType';
1112
import {
1213
CreateDeploymentParams,
@@ -100,6 +101,18 @@ const DescribeStackParamsSchema = z.object({
100101
stackName: CfnNameZodString,
101102
});
102103

104+
const DescribeEventsParamsSchema = z
105+
.object({
106+
stackName: CfnNameZodString.optional(),
107+
changeSetName: CfnNameZodString.optional(),
108+
operationId: z.string().optional(),
109+
failedEventsOnly: z.boolean().optional(),
110+
nextToken: z.string().optional(),
111+
})
112+
.refine((data) => data.stackName ?? data.changeSetName ?? data.operationId, {
113+
message: 'At least one of stackName, changeSetName, or operationId must be provided',
114+
});
115+
103116
export function parseCreateValidationParams(input: unknown): CreateValidationParams {
104117
return CreateValidationParamsSchema.parse(input);
105118
}
@@ -135,3 +148,7 @@ export function parseDescribeStackParams(input: unknown): DescribeStackParams {
135148
export function parseDescribeChangeSetParams(input: unknown): DescribeChangeSetParams {
136149
return DescribeChangeSetParamsSchema.parse(input);
137150
}
151+
152+
export function parseDescribeEventsParams(input: unknown): DescribeEventsParams {
153+
return DescribeEventsParamsSchema.parse(input);
154+
}

tst/unit/handlers/StackHandler.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getChangeSetDeletionStatusHandler,
3333
describeStackHandler,
3434
describeChangeSetHandler,
35+
describeEventsHandler,
3536
} from '../../../src/handlers/StackHandler';
3637
import { analyzeCapabilities } from '../../../src/stacks/actions/CapabilityAnalyzer';
3738
import { mapChangesToStackChanges } from '../../../src/stacks/actions/StackActionOperations';
@@ -51,6 +52,7 @@ import {
5152
DescribeStackResult,
5253
DescribeChangeSetParams,
5354
DescribeChangeSetResult,
55+
DescribeEventsResult,
5456
} from '../../../src/stacks/StackRequestType';
5557
import {
5658
createMockComponents,
@@ -82,6 +84,7 @@ vi.mock('../../../src/stacks/actions/StackActionParser', () => ({
8284
parseListStackResourcesParams: vi.fn((input) => input),
8385
parseDescribeStackParams: vi.fn((input) => input),
8486
parseDescribeChangeSetParams: vi.fn((input) => input),
87+
parseDescribeEventsParams: vi.fn((input) => input),
8588
}));
8689

8790
vi.mock('../../../src/utils/ZodErrorWrapper', () => ({
@@ -922,4 +925,58 @@ describe('StackActionHandler', () => {
922925
await expect(handler(params, {} as any)).rejects.toThrow('ChangeSet not found');
923926
});
924927
});
928+
929+
describe('describeEventsHandler', () => {
930+
it('should return flat events from API', async () => {
931+
mockComponents.cfnService.describeEvents.resolves({
932+
OperationEvents: [
933+
{ EventId: '1', OperationId: 'op1', Timestamp: new Date('2024-01-01') },
934+
{ EventId: '2', OperationId: 'op1', Timestamp: new Date('2024-01-02') },
935+
{ EventId: '3', OperationId: 'op2', Timestamp: new Date('2024-01-03') },
936+
],
937+
$metadata: {},
938+
});
939+
940+
const handler = describeEventsHandler(mockComponents);
941+
const result = (await handler({ stackName: 'test-stack' }, CancellationToken.None)) as DescribeEventsResult;
942+
943+
expect(result.events).toHaveLength(3);
944+
expect(result.events[0].EventId).toBe('1');
945+
});
946+
947+
it('should pass all parameters to API', async () => {
948+
mockComponents.cfnService.describeEvents.resolves({ OperationEvents: [], $metadata: {} });
949+
950+
const handler = describeEventsHandler(mockComponents);
951+
await handler(
952+
{
953+
stackName: 'test-stack',
954+
changeSetName: 'cs',
955+
operationId: 'op',
956+
failedEventsOnly: true,
957+
nextToken: 'token',
958+
},
959+
CancellationToken.None,
960+
);
961+
962+
expect(
963+
mockComponents.cfnService.describeEvents.calledWith({
964+
StackName: 'test-stack',
965+
ChangeSetName: 'cs',
966+
OperationId: 'op',
967+
FailedEventsOnly: true,
968+
NextToken: 'token',
969+
}),
970+
).toBe(true);
971+
});
972+
973+
it('should handle service errors', async () => {
974+
const serviceError = new Error('Service error');
975+
mockComponents.cfnService.describeEvents.rejects(serviceError);
976+
977+
const handler = describeEventsHandler(mockComponents);
978+
979+
await expect(handler({ stackName: 'test-stack' }, CancellationToken.None)).rejects.toThrow();
980+
});
981+
});
925982
});

tst/unit/stackActions/StackActionParser.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
parseCreateValidationParams,
66
parseTemplateUriParams,
77
parseDescribeChangeSetParams,
8+
parseDescribeEventsParams,
89
} from '../../../src/stacks/actions/StackActionParser';
910

1011
describe('StackActionParser', () => {
@@ -259,4 +260,66 @@ describe('StackActionParser', () => {
259260
expect(() => parseDescribeChangeSetParams(undefined)).toThrow(ZodError);
260261
});
261262
});
263+
264+
describe('parseDescribeEventsParams', () => {
265+
it('should parse valid params with stackName', () => {
266+
const result = parseDescribeEventsParams({ stackName: 'test-stack' });
267+
expect(result.stackName).toBe('test-stack');
268+
});
269+
270+
it('should parse valid params with changeSetName', () => {
271+
const result = parseDescribeEventsParams({ changeSetName: 'test-changeset' });
272+
expect(result.changeSetName).toBe('test-changeset');
273+
});
274+
275+
it('should parse valid params with operationId', () => {
276+
const result = parseDescribeEventsParams({ operationId: 'op-123' });
277+
expect(result.operationId).toBe('op-123');
278+
});
279+
280+
it('should parse all optional parameters', () => {
281+
const result = parseDescribeEventsParams({
282+
stackName: 'test-stack',
283+
changeSetName: 'test-changeset',
284+
operationId: 'op-123',
285+
failedEventsOnly: true,
286+
nextToken: 'token123',
287+
});
288+
289+
expect(result.stackName).toBe('test-stack');
290+
expect(result.changeSetName).toBe('test-changeset');
291+
expect(result.operationId).toBe('op-123');
292+
expect(result.failedEventsOnly).toBe(true);
293+
expect(result.nextToken).toBe('token123');
294+
});
295+
296+
it('should throw error when no identifier provided', () => {
297+
expect(() => parseDescribeEventsParams({})).toThrow();
298+
});
299+
300+
it('should throw error when only optional params provided', () => {
301+
expect(() => parseDescribeEventsParams({ failedEventsOnly: true })).toThrow();
302+
});
303+
304+
it('should throw error for invalid stackName', () => {
305+
expect(() => parseDescribeEventsParams({ stackName: '' })).toThrow();
306+
expect(() => parseDescribeEventsParams({ stackName: 123 as any })).toThrow();
307+
});
308+
309+
it('should throw error for invalid types', () => {
310+
expect(() => parseDescribeEventsParams({ stackName: 'test', failedEventsOnly: 'true' as any })).toThrow();
311+
});
312+
313+
it('should accept undefined optional parameters', () => {
314+
const result = parseDescribeEventsParams({
315+
stackName: 'test-stack',
316+
failedEventsOnly: undefined,
317+
nextToken: undefined,
318+
});
319+
320+
expect(result.stackName).toBe('test-stack');
321+
expect(result.failedEventsOnly).toBeUndefined();
322+
expect(result.nextToken).toBeUndefined();
323+
});
324+
});
262325
});

0 commit comments

Comments
 (0)