Skip to content
This repository was archived by the owner on Jan 16, 2025. It is now read-only.

Commit f2112ea

Browse files
authored
refactor: restructure webhook lambda (#3618)
Thie PR prepare for changes to easier migrate reading config from SSM instead of environment (#3594), add option to only accept messages of a defined IP list, and to introduce option to connect runners via EventBridg. - Validate input and throw validation exceptions if event cannot be accepted - Structure the code that to allow the webhook to be split in acceptiong an event and distribute to a runner (prepare for EventBridge). - Remove deprecated jest functions. - THE PR minimized changed on thest, only small structural things. This to ensure the test still validating the implemention
1 parent 13b1894 commit f2112ea

File tree

12 files changed

+296
-233
lines changed

12 files changed

+296
-233
lines changed

Diff for: lambdas/functions/webhook/jest.config.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ const config: Config = {
66
...defaultConfig,
77
coverageThreshold: {
88
global: {
9-
statements: 99.07,
10-
branches: 93.33,
9+
statements: 99.13,
10+
branches: 96.87,
1111
functions: 100,
12-
lines: 99.02,
12+
lines: 99.09,
1313
},
1414
},
1515
};

Diff for: lambdas/functions/webhook/src/ConfigResolver.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { QueueConfig } from './sqs';
2+
3+
export class Config {
4+
public repositoryAllowList: Array<string>;
5+
public queuesConfig: Array<QueueConfig>;
6+
public workflowJobEventSecondaryQueue;
7+
8+
constructor() {
9+
const repositoryAllowListEnv = process.env.REPOSITORY_ALLOW_LIST || '[]';
10+
this.repositoryAllowList = JSON.parse(repositoryAllowListEnv) as Array<string>;
11+
const queuesConfigEnv = process.env.RUNNER_CONFIG || '[]';
12+
this.queuesConfig = JSON.parse(queuesConfigEnv) as Array<QueueConfig>;
13+
this.workflowJobEventSecondaryQueue = process.env.WORKFLOW_JOB_EVENT_SECONDARY_QUEUE || undefined;
14+
}
15+
}

Diff for: lambdas/functions/webhook/src/ValidatonError.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class ValidationError extends Error {
2+
constructor(
3+
public statusCode: number,
4+
public message: string,
5+
public error?: Error,
6+
) {
7+
super(message);
8+
this.name = 'ValidationError';
9+
this.stack = error ? error.stack : new Error().stack;
10+
}
11+
}
12+
13+
export default ValidationError;

Diff for: lambdas/functions/webhook/src/lambda.test.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { APIGatewayEvent, Context } from 'aws-lambda';
33
import { mocked } from 'jest-mock';
44

55
import { githubWebhook } from './lambda';
6-
import { handle } from './webhook/handler';
6+
import { handle } from './webhook';
7+
import ValidationError from './ValidatonError';
78

89
const event: APIGatewayEvent = {
910
body: JSON.stringify(''),
@@ -71,7 +72,7 @@ const context: Context = {
7172
},
7273
};
7374

74-
jest.mock('./webhook/handler');
75+
jest.mock('./webhook');
7576

7677
describe('Test scale up lambda wrapper.', () => {
7778
it('Happy flow, resolve.', async () => {
@@ -88,14 +89,10 @@ describe('Test scale up lambda wrapper.', () => {
8889

8990
it('An expected error, resolve.', async () => {
9091
const mock = mocked(handle);
91-
mock.mockImplementation(() => {
92-
return new Promise((resolve) => {
93-
resolve({ statusCode: 400 });
94-
});
95-
});
92+
mock.mockRejectedValue(new ValidationError(400, 'some error'));
9693

9794
const result = await githubWebhook(event, context);
98-
expect(result).toEqual({ statusCode: 400 });
95+
expect(result).toMatchObject({ statusCode: 400 });
9996
});
10097

10198
it('Errors are not thrown.', async () => {

Diff for: lambdas/functions/webhook/src/lambda.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
import middy from '@middy/core';
2-
import { logger, setContext } from '@terraform-aws-github-runner/aws-powertools-util';
3-
import { captureLambdaHandler, tracer } from '@terraform-aws-github-runner/aws-powertools-util';
2+
import { logger, setContext, captureLambdaHandler, tracer } from '@terraform-aws-github-runner/aws-powertools-util';
43
import { APIGatewayEvent, Context } from 'aws-lambda';
54

6-
import { handle } from './webhook/handler';
5+
import { handle } from './webhook';
6+
import { Config } from './ConfigResolver';
7+
import { IncomingHttpHeaders } from 'http';
8+
import ValidationError from './ValidatonError';
79

810
export interface Response {
911
statusCode: number;
1012
body?: string;
1113
}
14+
1215
middy(githubWebhook).use(captureLambdaHandler(tracer));
16+
1317
export async function githubWebhook(event: APIGatewayEvent, context: Context): Promise<Response> {
1418
setContext(context, 'lambda.ts');
19+
const config = new Config();
20+
1521
logger.logEventIfEnabled(event);
22+
logger.debug('Loading config', { config });
1623

1724
let result: Response;
1825
try {
19-
result = await handle(event.headers, event.body as string);
26+
result = await handle(headersToLowerCase(event.headers), event.body as string, config);
2027
} catch (e) {
2128
logger.error(`Failed to handle webhook event`, { error: e });
22-
result = {
23-
statusCode: 500,
24-
body: 'Check the Lambda logs for the error details.',
25-
};
29+
if (e instanceof ValidationError) {
30+
result = {
31+
statusCode: e.statusCode,
32+
body: e.message,
33+
};
34+
} else {
35+
result = {
36+
statusCode: 500,
37+
body: 'Check the Lambda logs for the error details.',
38+
};
39+
}
2640
}
2741
return result;
2842
}
43+
44+
// ensure header keys lower case since github headers can contain capitals.
45+
function headersToLowerCase(headers: IncomingHttpHeaders): IncomingHttpHeaders {
46+
for (const key in headers) {
47+
headers[key.toLowerCase()] = headers[key];
48+
}
49+
return headers;
50+
}

Diff for: lambdas/functions/webhook/src/local.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import bodyParser from 'body-parser';
22
import express from 'express';
33

4-
import { handle } from './webhook/handler';
4+
import { handle } from './webhook';
5+
import { Config } from './ConfigResolver';
56

67
const app = express();
8+
const config = new Config();
79

810
app.use(bodyParser.json());
911

1012
app.post('/event_handler', (req, res) => {
11-
handle(req.headers, JSON.stringify(req.body))
13+
handle(req.headers, JSON.stringify(req.body), config)
1214
.then((c) => res.status(c.statusCode).end())
1315
.catch((e) => {
1416
console.log(e);

Diff for: lambdas/functions/webhook/src/sqs/index.test.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { SendMessageCommandInput } from '@aws-sdk/client-sqs';
22

33
import { ActionRequestMessage, GithubWorkflowEvent, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '.';
44
import workflowjob_event from '../../test/resources/github_workflowjob_event.json';
5+
import { Config } from '../ConfigResolver';
56

67
const mockSQS = {
78
sendMessage: jest.fn(() => {
8-
{
9-
return {};
10-
}
9+
return {};
1110
}),
1211
};
1312
jest.mock('@aws-sdk/client-sqs', () => ({
@@ -66,6 +65,7 @@ describe('Test sending message to SQS.', () => {
6665
expect(result).resolves;
6766
});
6867
});
68+
6969
describe('Test sending message to SQS.', () => {
7070
const message: GithubWorkflowEvent = {
7171
workflowJobEvent: JSON.parse(JSON.stringify(workflowjob_event)),
@@ -77,29 +77,36 @@ describe('Test sending message to SQS.', () => {
7777
afterEach(() => {
7878
jest.clearAllMocks();
7979
});
80+
8081
it('sends webhook events to workflow job queue', async () => {
8182
// Arrange
82-
process.env.SQS_WORKFLOW_JOB_QUEUE = sqsMessage.QueueUrl;
83+
process.env.WORKFLOW_JOB_EVENT_SECONDARY_QUEUE = sqsMessage.QueueUrl;
84+
const config = new Config();
8385

8486
// Act
85-
const result = await sendWebhookEventToWorkflowJobQueue(message);
87+
const result = await sendWebhookEventToWorkflowJobQueue(message, config);
8688

8789
// Assert
88-
expect(mockSQS.sendMessage).toBeCalledWith(sqsMessage);
90+
expect(mockSQS.sendMessage).toHaveBeenCalledWith(sqsMessage);
8991
expect(result).resolves;
9092
});
93+
9194
it('Does not send webhook events to workflow job event copy queue', async () => {
9295
// Arrange
93-
process.env.SQS_WORKFLOW_JOB_QUEUE = '';
96+
process.env.WORKFLOW_JOB_EVENT_SECONDARY_QUEUE = '';
97+
const config = new Config();
9498
// Act
95-
await sendWebhookEventToWorkflowJobQueue(message);
99+
await sendWebhookEventToWorkflowJobQueue(message, config);
96100

97101
// Assert
98-
expect(mockSQS.sendMessage).not.toBeCalledWith(sqsMessage);
102+
expect(mockSQS.sendMessage).not.toHaveBeenCalledWith(sqsMessage);
99103
});
104+
100105
it('Catch the exception when even copy queue throws exception', async () => {
101106
// Arrange
102-
process.env.SQS_WORKFLOW_JOB_QUEUE = sqsMessage.QueueUrl;
107+
process.env.WORKFLOW_JOB_EVENT_SECONDARY_QUEUE = sqsMessage.QueueUrl;
108+
const config = new Config();
109+
103110
const mockSQS = {
104111
sendMessage: jest.fn(() => {
105112
throw new Error();
@@ -108,7 +115,6 @@ describe('Test sending message to SQS.', () => {
108115
jest.mock('aws-sdk', () => ({
109116
SQS: jest.fn().mockImplementation(() => mockSQS),
110117
}));
111-
await expect(mockSQS.sendMessage).toThrowError();
112-
await expect(sendWebhookEventToWorkflowJobQueue(message)).resolves.not.toThrowError();
118+
await expect(sendWebhookEventToWorkflowJobQueue(message, config)).resolves.not.toThrow();
113119
});
114120
});

Diff for: lambdas/functions/webhook/src/sqs/index.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SQS, SendMessageCommandInput } from '@aws-sdk/client-sqs';
22
import { WorkflowJobEvent } from '@octokit/webhooks-types';
3-
import { createChildLogger } from '@terraform-aws-github-runner/aws-powertools-util';
4-
import { getTracedAWSV3Client } from '@terraform-aws-github-runner/aws-powertools-util';
3+
import { createChildLogger, getTracedAWSV3Client } from '@terraform-aws-github-runner/aws-powertools-util';
4+
import { Config } from '../ConfigResolver';
55

66
const logger = createChildLogger('sqs');
77

@@ -20,12 +20,15 @@ export interface MatcherConfig {
2020
exactMatch: boolean;
2121
}
2222

23+
export type RunnerConfig = QueueConfig[];
24+
2325
export interface QueueConfig {
2426
matcherConfig: MatcherConfig;
2527
id: string;
2628
arn: string;
2729
fifo: boolean;
2830
}
31+
2932
export interface GithubWorkflowEvent {
3033
workflowJobEvent: WorkflowJobEvent;
3134
}
@@ -46,20 +49,18 @@ export const sendActionRequest = async (message: ActionRequestMessage): Promise<
4649
await sqs.sendMessage(sqsMessage);
4750
};
4851

49-
export const sendWebhookEventToWorkflowJobQueue = async (message: GithubWorkflowEvent): Promise<void> => {
50-
const webhook_events_workflow_job_queue = process.env.SQS_WORKFLOW_JOB_QUEUE || undefined;
51-
52-
if (webhook_events_workflow_job_queue != undefined) {
52+
export async function sendWebhookEventToWorkflowJobQueue(message: GithubWorkflowEvent, config: Config): Promise<void> {
53+
if (config.workflowJobEventSecondaryQueue != undefined) {
5354
const sqs = new SQS({ region: process.env.AWS_REGION });
5455
const sqsMessage: SendMessageCommandInput = {
55-
QueueUrl: String(process.env.SQS_WORKFLOW_JOB_QUEUE),
56+
QueueUrl: String(config.workflowJobEventSecondaryQueue),
5657
MessageBody: JSON.stringify(message),
5758
};
58-
logger.debug(`Sending Webhook events to the workflow job queue: ${webhook_events_workflow_job_queue}`);
59+
logger.debug(`Sending Webhook events to the workflow job queue: ${config.workflowJobEventSecondaryQueue}`);
5960
try {
6061
await sqs.sendMessage(sqsMessage);
6162
} catch (e) {
6263
logger.warn(`Error in sending webhook events to workflow job queue: ${(e as Error).message}`);
6364
}
6465
}
65-
};
66+
}

0 commit comments

Comments
 (0)