Skip to content

Commit 285c362

Browse files
npalmgertjanmaas
andauthored
add webhook (#2)
* Add tf code for gateway / lambda * Remove gitkeep * Add sqs * Add queue and polcies * Add readme * Fix some errors * Send events to sqs * Add test lambda * Rename sqs url parameter * Cleanup * Add build dist command * Rework lambda a bit and add tests * Add ci for webhook * Update descriptions * Update path expression * Try working dir * Add terraform checks to ci * Fix validate * Build only on PR Co-authored-by: Gertjan Maas <[email protected]>
1 parent ed61f6b commit 285c362

25 files changed

+5243
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Lambda Agent Webhook
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
7+
paths:
8+
- .github/workflows/lambda-agent-webhook.yml
9+
- "modules/agent/lambdas/webhook/**"
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
container: node:12
15+
defaults:
16+
run:
17+
working-directory: modules/agent/lambdas/webhook
18+
19+
steps:
20+
- uses: actions/checkout@v2
21+
- name: Install dependencies
22+
run: yarn install
23+
- name: Run tests
24+
run: yarn test
25+
- name: Build distribution
26+
run: yarn build

.github/workflows/terraform.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: "Terraform root module checks"
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
7+
paths-ignore:
8+
- "modules/*/lambdas/**"
9+
10+
env:
11+
tf_version: "0.12.24"
12+
tf_working_dir: "."
13+
AWS_REGION: eu-west-1
14+
jobs:
15+
terraform:
16+
name: "Terraform"
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: "Checkout"
20+
uses: actions/checkout@v2
21+
- name: "Terraform Format"
22+
uses: hashicorp/terraform-github-actions@master
23+
with:
24+
tf_actions_version: ${{ env.tf_version }}
25+
tf_actions_subcommand: "fmt"
26+
tf_actions_working_dir: ${{ env.tf_working_dir }}
27+
tf_actions_comment: true
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
- name: "Terraform Init"
31+
uses: hashicorp/terraform-github-actions@master
32+
with:
33+
tf_actions_version: ${{ env.tf_version }}
34+
tf_actions_subcommand: "init"
35+
tf_actions_working_dir: ${{ env.tf_working_dir }}
36+
tf_actions_comment: true
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
- name: "Terraform Validate"
40+
uses: hashicorp/terraform-github-actions@master
41+
with:
42+
tf_actions_version: ${{ env.tf_version }}
43+
tf_actions_subcommand: "validate"
44+
tf_actions_working_dir: ${{ env.tf_working_dir }}
45+
tf_actions_comment: true
46+
env:
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

modules/agent/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Agent for orchestration of action runners
2+
3+
Agent to orchestrate the the action runners are composed of:
4+
- API Gatewway and lambda to receive GitHub events
5+
- SQS queue for decouple web hook to orchestrator
6+
- Lambda to create EC2 action runner instances based queue events and limits.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# dependencies
2+
node_modules/
3+
4+
# production
5+
dist/
6+
build/
7+
8+
# misc
9+
.DS_Store
10+
.env*
11+
*.zip
12+
13+
npm-debug.log*
14+
yarn-debug.log*
15+
yarn-error.log*

modules/agent/lambdas/webhook/.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v12.16.1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"printWidth": 120,
3+
"singleQuote": true,
4+
"trailingComma": "all"
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "github-runner-lambda-agent-webhook",
3+
"version": "1.0.0",
4+
"main": "lambda.ts",
5+
"license": "MIT",
6+
"scripts": {
7+
"start": "ts-node-dev src/local.ts",
8+
"test": "NODE_ENV=test jest",
9+
"watch": "ts-node-dev --respawn --exit-child src/local.ts",
10+
"build": "ncc build src/lambda.ts -o dist",
11+
"dist": "yarn build && cd dist && zip ../webhook.zip index.js"
12+
},
13+
"devDependencies": {
14+
"@octokit/webhooks": "^7.4.0",
15+
"@types/express": "^4.17.3",
16+
"@types/jest": "^25.2.1",
17+
"@types/node": "^13.13.4",
18+
"@zeit/ncc": "^0.22.1",
19+
"aws-sdk": "^2.645.0",
20+
"body-parser": "^1.19.0",
21+
"express": "^4.17.1",
22+
"jest": "^25.4.0",
23+
"ts-jest": "^25.4.0",
24+
"ts-node-dev": "^1.0.0-pre.44",
25+
"typescript": "^3.8.3"
26+
},
27+
"dependencies": {
28+
"crypto": "^1.0.1"
29+
}
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { handle as githubWebhook } from './webhook/handler';
2+
3+
module.exports.githubWebhook = async (event: any, context: any, callback: any) => {
4+
const statusCode = await githubWebhook(event.headers, event.body);
5+
return callback(null, {
6+
statusCode: statusCode,
7+
});
8+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import express from 'express';
2+
import bodyParser from 'body-parser';
3+
import { handle } from './webhook/handler';
4+
5+
const app = express();
6+
7+
app.use(bodyParser.json());
8+
9+
app.post('/event_handler', (req, res) => {
10+
handle(req.headers, JSON.stringify(req.body))
11+
.then((c) => res.status(c).end())
12+
.catch((e) => {
13+
console.log(e);
14+
res.status(404);
15+
});
16+
});
17+
18+
app.listen(3000, (): void => {
19+
console.log('webhook app listening on port 3000!');
20+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { SQS } from 'aws-sdk';
2+
import AWS from 'aws-sdk';
3+
4+
AWS.config.update({
5+
region: process.env.AWS_REGION,
6+
});
7+
8+
const sqs = new SQS();
9+
10+
export interface ActionRequestMessage {
11+
id: number;
12+
eventType: string;
13+
repositoryName: string;
14+
repositoryOwner: string;
15+
installationId: number;
16+
}
17+
18+
export const sendActionRequest = async (message: ActionRequestMessage) => {
19+
await sqs
20+
.sendMessage({
21+
QueueUrl: String(process.env.SQS_URL_WEBHOOK),
22+
MessageBody: JSON.stringify(message),
23+
MessageGroupId: String(message.id),
24+
})
25+
.promise();
26+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { handle } from './handler';
2+
import check_run_event from '../../test/resources/github_check_run_event.json';
3+
4+
import { sendActionRequest } from '../sqs';
5+
6+
jest.mock('../sqs');
7+
8+
describe('handler', () => {
9+
let originalError: Console['error'];
10+
11+
beforeEach(() => {
12+
process.env.GITHUB_APP_WEBHOOK_SECRET = 'TEST_SECRET';
13+
originalError = console.error;
14+
console.error = jest.fn();
15+
jest.clearAllMocks();
16+
});
17+
18+
afterEach(() => {
19+
console.error = originalError;
20+
});
21+
22+
it('returns 500 if no signature available', async () => {
23+
const resp = await handle({}, '');
24+
expect(resp).toBe(500);
25+
});
26+
27+
it('returns 401 if signature is invalid', async () => {
28+
const resp = await handle({ 'X-Hub-Signature': 'bbb' }, 'aaaa');
29+
expect(resp).toBe(401);
30+
});
31+
32+
it('handles check_run events', async () => {
33+
const resp = await handle(
34+
{ 'X-Hub-Signature': 'sha1=4a82d2f60346e16dab3546eb3b56d8dde4d5b659', 'X-GitHub-Event': 'check_run' },
35+
JSON.stringify(check_run_event),
36+
);
37+
expect(resp).toBe(200);
38+
expect(sendActionRequest).toBeCalled();
39+
});
40+
41+
it('does not handle other events', async () => {
42+
const resp = await handle(
43+
{ 'X-Hub-Signature': 'sha1=4a82d2f60346e16dab3546eb3b56d8dde4d5b659', 'X-GitHub-Event': 'push' },
44+
JSON.stringify(check_run_event),
45+
);
46+
expect(resp).toBe(200);
47+
expect(sendActionRequest).not.toBeCalled();
48+
});
49+
50+
it('does not handle check_run events with actions other than created', async () => {
51+
const event = { ...check_run_event, action: 'completed' };
52+
const resp = await handle(
53+
{ 'X-Hub-Signature': 'sha1=891749859807857017f7ee56a429e8fcead6f3e1', 'X-GitHub-Event': 'push' },
54+
JSON.stringify(event),
55+
);
56+
expect(resp).toBe(200);
57+
expect(sendActionRequest).not.toBeCalled();
58+
});
59+
60+
it('does not handle check_run events with status other than queued', async () => {
61+
const event = { ...check_run_event, check_run: { id: 1234, status: 'completed' } };
62+
const resp = await handle(
63+
{ 'X-Hub-Signature': 'sha1=73dfae4aa56de5b038af8921b40d7a412ce7ca19', 'X-GitHub-Event': 'push' },
64+
JSON.stringify(event),
65+
);
66+
expect(resp).toBe(200);
67+
expect(sendActionRequest).not.toBeCalled();
68+
});
69+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { IncomingHttpHeaders } from 'http';
2+
import crypto from 'crypto';
3+
import { sendActionRequest } from '../sqs';
4+
import { WebhookPayloadCheckRun } from '@octokit/webhooks';
5+
6+
function signRequestBody(key: string, body: any) {
7+
return `sha1=${crypto.createHmac('sha1', key).update(body, 'utf8').digest('hex')}`;
8+
}
9+
10+
export const handle = async (headers: IncomingHttpHeaders, payload: any): Promise<number> => {
11+
// ensure header keys lower case since github headers can contain capitals.
12+
for (const key in headers) {
13+
headers[key.toLowerCase()] = headers[key];
14+
}
15+
16+
const secret = process.env.GITHUB_APP_WEBHOOK_SECRET as string;
17+
const signature = headers['x-hub-signature'];
18+
if (!signature) {
19+
console.error("Github event doesn't have signature. This webhook requires a secret to be configured.");
20+
return 500;
21+
}
22+
23+
const calculatedSig = signRequestBody(secret, payload);
24+
if (signature !== calculatedSig) {
25+
console.error('Unable to verify signature!');
26+
return 401;
27+
}
28+
29+
const githubEvent = headers['x-github-event'];
30+
31+
console.debug(`Received Github event: "${githubEvent}"`);
32+
33+
if (githubEvent === 'check_run') {
34+
const body = JSON.parse(payload) as WebhookPayloadCheckRun;
35+
if (body.action === 'created' && body.check_run.status === 'queued') {
36+
await sendActionRequest({
37+
id: body.check_run.id,
38+
repositoryName: body.repository.name,
39+
repositoryOwner: body.repository.owner.login,
40+
eventType: githubEvent,
41+
installationId: body.installation!.id,
42+
});
43+
}
44+
} else {
45+
console.debug('Ignore event ' + githubEvent);
46+
}
47+
48+
return 200;
49+
};

0 commit comments

Comments
 (0)