Skip to content
Merged
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
28 changes: 28 additions & 0 deletions src/handlers/StackHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
parseGetStackEventsParams,
parseClearStackEventsParams,
parseDescribeStackParams,
parseDescribeEventsParams,
} from '../stacks/actions/StackActionParser';
import {
TemplateUri,
Expand Down Expand Up @@ -48,6 +49,8 @@ import {
DescribeStackResult,
DescribeChangeSetParams,
DescribeChangeSetResult,
DescribeEventsParams,
DescribeEventsResult,
} from '../stacks/StackRequestType';
import { TelemetryService } from '../telemetry/TelemetryService';
import { EventType } from '../usageTracker/UsageTracker';
Expand Down Expand Up @@ -444,3 +447,28 @@ export function describeStackHandler(
}
};
}

export function describeEventsHandler(
components: ServerComponents,
): RequestHandler<DescribeEventsParams, DescribeEventsResult, void> {
return async (rawParams): Promise<DescribeEventsResult> => {
try {
const params = parseWithPrettyError(parseDescribeEventsParams, rawParams);

const response = await components.cfnService.describeEvents({
StackName: params.stackName,
ChangeSetName: params.changeSetName,
OperationId: params.operationId,
FailedEventsOnly: params.failedEventsOnly,
NextToken: params.nextToken,
});

return {
events: response.OperationEvents ?? [],
nextToken: response.NextToken,
};
} catch (error) {
handleLspError(error, 'Failed to describe events');
}
};
}
7 changes: 7 additions & 0 deletions src/protocol/LspStackHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ import {
DescribeChangeSetParams,
DescribeChangeSetResult,
DescribeChangeSetRequest,
DescribeEventsParams,
DescribeEventsResult,
DescribeEventsRequest,
} from '../stacks/StackRequestType';
import { Identifiable } from './LspTypes';

Expand Down Expand Up @@ -142,4 +145,8 @@ export class LspStackHandlers {
onDescribeStack(handler: RequestHandler<DescribeStackParams, DescribeStackResult, void>) {
this.connection.onRequest(DescribeStackRequest.method, handler);
}

onDescribeEvents(handler: RequestHandler<DescribeEventsParams, DescribeEventsResult, void>) {
this.connection.onRequest(DescribeEventsRequest.method, handler);
}
}
7 changes: 7 additions & 0 deletions src/server/CfnServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
clearStackEventsHandler,
describeStackHandler,
describeChangeSetHandler,
describeEventsHandler,
} from '../handlers/StackHandler';
import { LspComponents } from '../protocol/LspComponents';
import { LoggerFactory } from '../telemetry/LoggerFactory';
Expand Down Expand Up @@ -228,6 +229,12 @@ export class CfnServer {
withOnlineGuard(this.components.onlineFeatureGuard, describeStackHandler(this.components)),
),
);
this.lsp.stackHandlers.onDescribeEvents(
withTelemetryContext(
'Stack.Describe.Events',
withOnlineGuard(this.components.onlineFeatureGuard, describeEventsHandler(this.components)),
),
);

this.lsp.cfnEnvironmentHandlers.onParseCfnEnvironmentFiles(
withTelemetryContext('Cfn.Environment.Parse', parseCfnEnvironmentFilesHandler()),
Expand Down
26 changes: 25 additions & 1 deletion src/stacks/StackRequestType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { StackSummary, StackStatus, StackResourceSummary, StackEvent, Stack } from '@aws-sdk/client-cloudformation';
import {
StackSummary,
StackStatus,
StackResourceSummary,
StackEvent,
Stack,
OperationEvent,
} from '@aws-sdk/client-cloudformation';
import { RequestType } from 'vscode-languageserver-protocol';
import { ChangeSetReference, DeploymentMode, StackChange } from './actions/StackActionRequestType';

Expand Down Expand Up @@ -111,3 +118,20 @@ export const DescribeStackRequest = new RequestType<DescribeStackParams, Describ
export const DescribeChangeSetRequest = new RequestType<DescribeChangeSetParams, DescribeChangeSetResult, void>(
'aws/cfn/stack/changeSet/describe',
);

export type DescribeEventsParams = {
stackName?: string;
changeSetName?: string;
operationId?: string;
failedEventsOnly?: boolean;
nextToken?: string;
};

export type DescribeEventsResult = {
events: OperationEvent[];
nextToken?: string;
};

export const DescribeEventsRequest = new RequestType<DescribeEventsParams, DescribeEventsResult, void>(
'aws/cfn/stack/events/describe',
);
17 changes: 17 additions & 0 deletions src/stacks/actions/StackActionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ClearStackEventsParams,
DescribeStackParams,
DescribeChangeSetParams,
DescribeEventsParams,
} from '../StackRequestType';
import {
CreateDeploymentParams,
Expand Down Expand Up @@ -100,6 +101,18 @@ const DescribeStackParamsSchema = z.object({
stackName: CfnNameZodString,
});

const DescribeEventsParamsSchema = z
.object({
stackName: CfnNameZodString.optional(),
changeSetName: CfnNameZodString.optional(),
operationId: z.string().optional(),
failedEventsOnly: z.boolean().optional(),
nextToken: z.string().optional(),
})
.refine((data) => data.stackName ?? data.changeSetName ?? data.operationId, {
message: 'At least one of stackName, changeSetName, or operationId must be provided',
});

export function parseCreateValidationParams(input: unknown): CreateValidationParams {
return CreateValidationParamsSchema.parse(input);
}
Expand Down Expand Up @@ -135,3 +148,7 @@ export function parseDescribeStackParams(input: unknown): DescribeStackParams {
export function parseDescribeChangeSetParams(input: unknown): DescribeChangeSetParams {
return DescribeChangeSetParamsSchema.parse(input);
}

export function parseDescribeEventsParams(input: unknown): DescribeEventsParams {
return DescribeEventsParamsSchema.parse(input);
}
57 changes: 57 additions & 0 deletions tst/unit/handlers/StackHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getChangeSetDeletionStatusHandler,
describeStackHandler,
describeChangeSetHandler,
describeEventsHandler,
} from '../../../src/handlers/StackHandler';
import { analyzeCapabilities } from '../../../src/stacks/actions/CapabilityAnalyzer';
import { mapChangesToStackChanges } from '../../../src/stacks/actions/StackActionOperations';
Expand All @@ -51,6 +52,7 @@ import {
DescribeStackResult,
DescribeChangeSetParams,
DescribeChangeSetResult,
DescribeEventsResult,
} from '../../../src/stacks/StackRequestType';
import {
createMockComponents,
Expand Down Expand Up @@ -82,6 +84,7 @@ vi.mock('../../../src/stacks/actions/StackActionParser', () => ({
parseListStackResourcesParams: vi.fn((input) => input),
parseDescribeStackParams: vi.fn((input) => input),
parseDescribeChangeSetParams: vi.fn((input) => input),
parseDescribeEventsParams: vi.fn((input) => input),
}));

vi.mock('../../../src/utils/ZodErrorWrapper', () => ({
Expand Down Expand Up @@ -922,4 +925,58 @@ describe('StackActionHandler', () => {
await expect(handler(params, {} as any)).rejects.toThrow('ChangeSet not found');
});
});

describe('describeEventsHandler', () => {
it('should return flat events from API', async () => {
mockComponents.cfnService.describeEvents.resolves({
OperationEvents: [
{ EventId: '1', OperationId: 'op1', Timestamp: new Date('2024-01-01') },
{ EventId: '2', OperationId: 'op1', Timestamp: new Date('2024-01-02') },
{ EventId: '3', OperationId: 'op2', Timestamp: new Date('2024-01-03') },
],
$metadata: {},
});

const handler = describeEventsHandler(mockComponents);
const result = (await handler({ stackName: 'test-stack' }, CancellationToken.None)) as DescribeEventsResult;

expect(result.events).toHaveLength(3);
expect(result.events[0].EventId).toBe('1');
});

it('should pass all parameters to API', async () => {
mockComponents.cfnService.describeEvents.resolves({ OperationEvents: [], $metadata: {} });

const handler = describeEventsHandler(mockComponents);
await handler(
{
stackName: 'test-stack',
changeSetName: 'cs',
operationId: 'op',
failedEventsOnly: true,
nextToken: 'token',
},
CancellationToken.None,
);

expect(
mockComponents.cfnService.describeEvents.calledWith({
StackName: 'test-stack',
ChangeSetName: 'cs',
OperationId: 'op',
FailedEventsOnly: true,
NextToken: 'token',
}),
).toBe(true);
});

it('should handle service errors', async () => {
const serviceError = new Error('Service error');
mockComponents.cfnService.describeEvents.rejects(serviceError);

const handler = describeEventsHandler(mockComponents);

await expect(handler({ stackName: 'test-stack' }, CancellationToken.None)).rejects.toThrow();
});
});
});
63 changes: 63 additions & 0 deletions tst/unit/stackActions/StackActionParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
parseCreateValidationParams,
parseTemplateUriParams,
parseDescribeChangeSetParams,
parseDescribeEventsParams,
} from '../../../src/stacks/actions/StackActionParser';

describe('StackActionParser', () => {
Expand Down Expand Up @@ -259,4 +260,66 @@ describe('StackActionParser', () => {
expect(() => parseDescribeChangeSetParams(undefined)).toThrow(ZodError);
});
});

describe('parseDescribeEventsParams', () => {
it('should parse valid params with stackName', () => {
const result = parseDescribeEventsParams({ stackName: 'test-stack' });
expect(result.stackName).toBe('test-stack');
});

it('should parse valid params with changeSetName', () => {
const result = parseDescribeEventsParams({ changeSetName: 'test-changeset' });
expect(result.changeSetName).toBe('test-changeset');
});

it('should parse valid params with operationId', () => {
const result = parseDescribeEventsParams({ operationId: 'op-123' });
expect(result.operationId).toBe('op-123');
});

it('should parse all optional parameters', () => {
const result = parseDescribeEventsParams({
stackName: 'test-stack',
changeSetName: 'test-changeset',
operationId: 'op-123',
failedEventsOnly: true,
nextToken: 'token123',
});

expect(result.stackName).toBe('test-stack');
expect(result.changeSetName).toBe('test-changeset');
expect(result.operationId).toBe('op-123');
expect(result.failedEventsOnly).toBe(true);
expect(result.nextToken).toBe('token123');
});

it('should throw error when no identifier provided', () => {
expect(() => parseDescribeEventsParams({})).toThrow();
});

it('should throw error when only optional params provided', () => {
expect(() => parseDescribeEventsParams({ failedEventsOnly: true })).toThrow();
});

it('should throw error for invalid stackName', () => {
expect(() => parseDescribeEventsParams({ stackName: '' })).toThrow();
expect(() => parseDescribeEventsParams({ stackName: 123 as any })).toThrow();
});

it('should throw error for invalid types', () => {
expect(() => parseDescribeEventsParams({ stackName: 'test', failedEventsOnly: 'true' as any })).toThrow();
});

it('should accept undefined optional parameters', () => {
const result = parseDescribeEventsParams({
stackName: 'test-stack',
failedEventsOnly: undefined,
nextToken: undefined,
});

expect(result.stackName).toBe('test-stack');
expect(result.failedEventsOnly).toBeUndefined();
expect(result.nextToken).toBeUndefined();
});
});
});