From 5facc717166accaa03eb0d0447e797d8820e0b54 Mon Sep 17 00:00:00 2001 From: Gertjan Maas Date: Wed, 6 May 2020 13:17:47 +0200 Subject: [PATCH] WIP: Scale lambda --- .github/workflows/lambda-scale-runners.yml | 26 ++++++++++ .../lambdas/scale-runners/package.json | 5 +- .../lambdas/scale-runners/src/lambda.ts | 11 +++-- .../src/scale-runners/handler.test.ts | 47 ++++++++++++++++++ .../src/scale-runners/handler.ts | 49 +++++++++++++++++-- 5 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/lambda-scale-runners.yml create mode 100644 modules/runners/lambdas/scale-runners/src/scale-runners/handler.test.ts diff --git a/.github/workflows/lambda-scale-runners.yml b/.github/workflows/lambda-scale-runners.yml new file mode 100644 index 0000000000..81152834bf --- /dev/null +++ b/.github/workflows/lambda-scale-runners.yml @@ -0,0 +1,26 @@ +name: Lambda Scale Runners +on: + push: + branches: + - master + pull_request: + paths: + - .github/workflows/lambda-scale-runners.yml + - "modules/runners/lambdas/scale-runners/**" + +jobs: + build: + runs-on: ubuntu-latest + container: node:12 + defaults: + run: + working-directory: modules/runners/lambdas/scale-runners + + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: yarn install + - name: Run tests + run: yarn test + - name: Build distribution + run: yarn build diff --git a/modules/runners/lambdas/scale-runners/package.json b/modules/runners/lambdas/scale-runners/package.json index 1c6ee2fe6d..28d3449033 100644 --- a/modules/runners/lambdas/scale-runners/package.json +++ b/modules/runners/lambdas/scale-runners/package.json @@ -23,5 +23,8 @@ "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" }, - "dependencies": {} + "dependencies": { + "@octokit/auth-app": "^2.4.5", + "@octokit/rest": "^17.6.0" + } } diff --git a/modules/runners/lambdas/scale-runners/src/lambda.ts b/modules/runners/lambdas/scale-runners/src/lambda.ts index 31f8a2c7a9..ae80085202 100644 --- a/modules/runners/lambdas/scale-runners/src/lambda.ts +++ b/modules/runners/lambdas/scale-runners/src/lambda.ts @@ -1,8 +1,11 @@ import { handle } from './scale-runners/handler'; module.exports.handler = async (event: any, context: any, callback: any) => { - const statusCode = await handle(event.headers, event.body); - return callback(null, { - statusCode: statusCode, - }); + try { + await handle(event.eventSource, JSON.parse(event.body)); + return callback(null); + } catch (e) { + console.error(e); + return callback('Failed handling SQS event'); + } }; diff --git a/modules/runners/lambdas/scale-runners/src/scale-runners/handler.test.ts b/modules/runners/lambdas/scale-runners/src/scale-runners/handler.test.ts new file mode 100644 index 0000000000..c64d3f8807 --- /dev/null +++ b/modules/runners/lambdas/scale-runners/src/scale-runners/handler.test.ts @@ -0,0 +1,47 @@ +import { ActionRequestMessage, handle } from './handler'; + +import { createAppAuth } from '@octokit/auth-app'; +import { Octokit } from '@octokit/rest'; + +jest.mock('@octokit/auth-app', () => ({ + createAppAuth: jest.fn().mockImplementation(() => jest.fn().mockImplementation(() => ({ token: 'Blaat' }))), +})); +const mockOctokit = { checks: { get: jest.fn() }, actions: { listRepoWorkflowRuns: jest.fn() } }; +jest.mock('@octokit/rest', () => ({ + Octokit: jest.fn().mockImplementation(() => mockOctokit), +})); + +const TEST_DATA: ActionRequestMessage = { + id: 1, + eventType: 'check_run', + repositoryName: 'hello-world', + repositoryOwner: 'Codertocat', + installationId: 2, +}; + +describe('handler', () => { + beforeEach(() => { + process.env.GITHUB_APP_KEY = 'TEST_CERTIFICATE_DATA'; + process.env.GITHUB_APP_ID = '1337'; + process.env.GITHUB_APP_CLIENT_ID = 'TEST_CLIENT_ID'; + process.env.GITHUB_APP_CLIENT_SECRET = 'TEST_CLIENT_SECRET'; + jest.clearAllMocks(); + mockOctokit.actions.listRepoWorkflowRuns.mockImplementation(() => ({ + total_count: 1, + })); + }); + + it('ignores non-sqs events', async () => { + expect.assertions(1); + expect(handle('aws:s3', TEST_DATA)).rejects.toEqual(Error('Cannot handle non-SQS events!')); + }); + + it('checks queued workflows', async () => { + await handle('aws:sqs', TEST_DATA); + expect(mockOctokit.actions.listRepoWorkflowRuns).toBeCalledWith({ + owner: TEST_DATA.repositoryOwner, + repo: TEST_DATA.repositoryName, + status: 'queued', + }); + }); +}); diff --git a/modules/runners/lambdas/scale-runners/src/scale-runners/handler.ts b/modules/runners/lambdas/scale-runners/src/scale-runners/handler.ts index 2d738ef0fd..fe5acf2122 100644 --- a/modules/runners/lambdas/scale-runners/src/scale-runners/handler.ts +++ b/modules/runners/lambdas/scale-runners/src/scale-runners/handler.ts @@ -1,5 +1,48 @@ -import { IncomingHttpHeaders } from 'http'; +import { createAppAuth } from '@octokit/auth-app'; +import { Octokit } from '@octokit/rest'; -export const handle = async (headers: IncomingHttpHeaders, payload: any): Promise => { - return 200; +export interface ActionRequestMessage { + id: number; + eventType: string; + repositoryName: string; + repositoryOwner: string; + installationId: number; +} + +async function createGithubClient(installationId: number): Promise { + const privateKey = process.env.GITHUB_APP_KEY as string; + const appId: number = parseInt(process.env.GITHUB_APP_ID as string); + const clientId = process.env.GITHUB_APP_CLIENT_ID as string; + const clientSecret = process.env.GITHUB_APP_CLIENT_SECRET as string; + + try { + const auth = createAppAuth({ + id: appId, + privateKey: privateKey, + installationId: installationId, + clientId: clientId, + clientSecret: clientSecret, + }); + const installationAuthentication = await auth({ type: 'installation' }); + + return new Octokit({ + auth: installationAuthentication.token, + }); + } catch (e) { + Promise.reject(e); + } +} + +export const handle = async (eventSource: string, payload: ActionRequestMessage): Promise => { + if (eventSource !== 'aws:sqs') throw Error('Cannot handle non-SQS events!'); + const githubClient = await createGithubClient(payload.installationId); + const queuedWorkflows = await githubClient.actions.listRepoWorkflowRuns({ + owner: payload.repositoryOwner, + repo: payload.repositoryName, + // @ts-ignore (typing is incorrect) + status: 'queued', + }); + console.info( + `Repo ${payload.repositoryOwner}/${payload.repositoryName} has ${queuedWorkflows.total_count} queued workflow runs`, + ); };