Skip to content

Commit 1f105a9

Browse files
authored
feat(ghes): Support for GitHub Enterprise Server (#412)
* feat(ghes): Support for GitHub Enterprise Server - Updates lambdas to support GHES URL - Updates TF to support GHES and deploying lambda in VPC * addressing feedback * Remove extra comma * additional fixes * correcting merge * Require semi-colon Consisent format requirements
1 parent 602efc9 commit 1f105a9

30 files changed

+5500
-259
lines changed

.ci/.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

.ci/build.sh

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash
2+
set -e
23

34
lambdaSrcDirs=("modules/runner-binaries-syncer/lambdas/runner-binaries-syncer" "modules/runners/lambdas/runners" "modules/webhook/lambdas/webhook")
45
repoRoot=$(dirname $(dirname $(realpath ${BASH_SOURCE[0]})))

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ example/*.secrets*.tfvars
1717
*.gz
1818
*.tgz
1919
*.env
20+
.vscode
21+
22+
**/coverage/*

README.md

+76-63
Large diffs are not rendered by default.

main.tf

+5
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ module "runners" {
9292
lambda_zip = var.runners_lambda_zip
9393
lambda_timeout_scale_up = var.runners_scale_up_lambda_timeout
9494
lambda_timeout_scale_down = var.runners_scale_down_lambda_timeout
95+
lambda_subnet_ids = var.lambda_subnet_ids
96+
lambda_security_group_ids = var.lambda_security_group_ids
9597
logging_retention_in_days = var.logging_retention_in_days
9698
enable_cloudwatch_agent = var.enable_cloudwatch_agent
9799
cloudwatch_config = var.cloudwatch_config
@@ -104,10 +106,13 @@ module "runners" {
104106
userdata_template = var.userdata_template
105107
userdata_pre_install = var.userdata_pre_install
106108
userdata_post_install = var.userdata_post_install
109+
key_name = var.key_name
107110

108111
create_service_linked_role_spot = var.create_service_linked_role_spot
109112

110113
runner_iam_role_managed_policy_arns = var.runner_iam_role_managed_policy_arns
114+
115+
ghes_url = var.ghes_url
111116
}
112117

113118
module "runner_binaries" {

modules/download-lambda/.terraform.lock.hcl

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

modules/runner-binaries-syncer/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ No requirements.
5353
| environment | A name that identifies the environment, used as prefix and for tagging. | `string` | n/a | yes |
5454
| lambda\_s3\_bucket | S3 bucket from which to specify lambda functions. This is an alternative to providing local files directly. | `any` | `null` | no |
5555
| lambda\_schedule\_expression | Scheduler expression for action runner binary syncer. | `string` | `"cron(27 * * * ? *)"` | no |
56+
| lambda\_security\_group\_ids | List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | `[]` | no |
57+
| lambda\_subnet\_ids | List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | `[]` | no |
5658
| lambda\_timeout | Time out of the lambda in seconds. | `number` | `300` | no |
5759
| lambda\_zip | File location of the lambda zip file. | `string` | `null` | no |
5860
| logging\_retention\_in\_days | Specifies the number of days you want to retain log events for the lambda log group. Possible values are: 0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653. | `number` | `7` | no |
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"printWidth": 120,
33
"singleQuote": true,
4-
"trailingComma": "all"
4+
"trailingComma": "all",
5+
"semi": true,
56
}
7+

modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"lint": "yarn eslint --ext ts,tsx src",
1111
"watch": "ts-node-dev --respawn --exit-child src/local.ts",
1212
"build": "ncc build src/lambda.ts -o dist",
13-
"dist": "yarn build && cd dist && zip ../runner-binaries-syncer.zip index.js"
13+
"dist": "yarn build && cd dist && zip ../runner-binaries-syncer.zip index.js",
14+
"format": "prettier --write \"**/*.ts\"",
15+
"format-check": "prettier --check \"**/*.ts\""
1416
},
1517
"devDependencies": {
1618
"@octokit/rest": "^18.0.12",

modules/runner-binaries-syncer/runner-binaries-syncer.tf

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ resource "aws_lambda_function" "syncer" {
2323
GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = var.runner_allow_prerelease_binaries
2424
}
2525
}
26+
dynamic "vpc_config" {
27+
for_each = var.lambda_subnet_ids != null && var.lambda_security_group_ids != null ? [true] : []
28+
content {
29+
security_group_ids = var.lambda_security_group_ids
30+
subnet_ids = var.lambda_subnet_ids
31+
}
32+
}
2633

2734
tags = var.tags
2835
}

modules/runner-binaries-syncer/variables.tf

+11
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,14 @@ variable "syncer_lambda_s3_object_version" {
8282
default = null
8383
}
8484

85+
variable "lambda_subnet_ids" {
86+
description = "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`."
87+
type = list(string)
88+
default = []
89+
}
90+
91+
variable "lambda_security_group_ids" {
92+
description = "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`."
93+
type = list(string)
94+
default = []
95+
}

modules/runners/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,15 @@ No requirements.
7373
| enable\_ssm\_on\_runners | Enable to allow access the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances. | `bool` | n/a | yes |
7474
| encryption | KMS key to encrypted lambda environment secrets. Either provide a key and `encrypt` set to `true`. Or set the key to `null` and encrypt to `false`. | <pre>object({<br> kms_key_id = string<br> encrypt = bool<br> })</pre> | n/a | yes |
7575
| environment | A name that identifies the environment, used as prefix and for tagging. | `string` | n/a | yes |
76+
| ghes\_url | GitHub Enterprise Server URL. DO NOT SET IF USING PUBLIC GITHUB | `string` | `null` | no |
7677
| github\_app | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). | <pre>object({<br> key_base64 = string<br> id = string<br> client_id = string<br> client_secret = string<br> })</pre> | n/a | yes |
7778
| idle\_config | List of time period that can be defined as cron expression to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle. | <pre>list(object({<br> cron = string<br> timeZone = string<br> idleCount = number<br> }))</pre> | `[]` | no |
7879
| instance\_profile\_path | The path that will be added to the instance\_profile, if not set the environment name will be used. | `string` | `null` | no |
7980
| instance\_type | Default instance type for the action runner. | `string` | `"m5.large"` | no |
81+
| key\_name | Key pair name | `string` | `null` | no |
8082
| lambda\_s3\_bucket | S3 bucket from which to specify lambda functions. This is an alternative to providing local files directly. | `any` | `null` | no |
83+
| lambda\_security\_group\_ids | List of subnets in which the lambda will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | `[]` | no |
84+
| lambda\_subnet\_ids | List of subnets in which the lambda will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | `[]` | no |
8185
| lambda\_timeout\_scale\_down | Time out for the scale down lambda in seconds. | `number` | `60` | no |
8286
| lambda\_timeout\_scale\_up | Time out for the scale up lambda in seconds. | `number` | `60` | no |
8387
| lambda\_zip | File location of the lambda zip file. | `string` | `null` | no |
+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"printWidth": 120,
33
"singleQuote": true,
4-
"trailingComma": "all"
4+
"trailingComma": "all",
5+
"semi": true,
56
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
module.exports = {
22
preset: 'ts-jest',
33
testEnvironment: 'node',
4+
collectCoverage: true,
5+
collectCoverageFrom: ['src/**/*.{ts,js,jsx}'],
6+
coverageThreshold: {
7+
global: {
8+
branches: 80,
9+
functions: 80,
10+
lines: 80,
11+
statements: 80
12+
}
13+
}
414
};

modules/runners/lambdas/runners/package.json

+12-5
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,32 @@
88
"test": "NODE_ENV=test jest",
99
"watch": "ts-node-dev --respawn --exit-child src/local.ts",
1010
"build": "ncc build src/lambda.ts -o dist",
11-
"dist": "yarn build && cd dist && zip ../runners.zip index.js"
11+
"dist": "yarn build && cd dist && zip ../runners.zip index.js",
12+
"format": "prettier --write \"**/*.ts\"",
13+
"format-check": "prettier --check \"**/*.ts\""
1214
},
1315
"devDependencies": {
1416
"@types/aws-lambda": "^8.10.68",
1517
"@types/express": "^4.17.9",
1618
"@types/jest": "^26.0.19",
17-
"@types/node": "^14.14.16",
1819
"@vercel/ncc": "^0.26.1",
19-
"aws-sdk": "^2.817.0",
2020
"jest": "^26.6.3",
21+
"jest-mock-extended": "^1.0.10",
22+
"nock": "^13.0.5",
2123
"ts-jest": "^26.4.4",
22-
"ts-node-dev": "^1.1.1",
23-
"typescript": "^4.1.3"
24+
"ts-node-dev": "^1.1.1"
2425
},
2526
"dependencies": {
2627
"@octokit/auth-app": "^2.10.5",
2728
"@octokit/rest": "^18.0.12",
29+
"@octokit/types": "^6.1.1",
30+
"@types/aws-lambda": "^8.10.68",
31+
"@types/express": "^4.17.9",
32+
"@types/node": "^14.14.16",
33+
"aws-sdk": "^2.817.0",
2834
"cron-parser": "^2.18.0",
2935
"moment": "^2.29.1",
36+
"typescript": "^4.1.3",
3037
"yn": "^4.0.0"
3138
}
3239
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { createOctoClient, createGithubAuth } from './gh-auth';
2+
import nock from 'nock';
3+
import { createAppAuth } from '@octokit/auth-app';
4+
import { StrategyOptions } from '@octokit/auth-app/dist-types/types';
5+
import { decrypt } from './kms';
6+
import { RequestInterface } from '@octokit/types';
7+
import { mock, MockProxy } from 'jest-mock-extended';
8+
import { request } from '@octokit/request';
9+
10+
jest.mock('./kms');
11+
jest.mock('@octokit/auth-app');
12+
13+
const cleanEnv = process.env;
14+
15+
beforeEach(() => {
16+
jest.resetModules();
17+
jest.clearAllMocks();
18+
process.env = { ...cleanEnv };
19+
nock.disableNetConnect();
20+
});
21+
22+
describe('Test createGithubAuth', () => {
23+
test('Creates app client to GitHub public', async () => {
24+
// Arrange
25+
const token = '123456';
26+
27+
// Act
28+
const result = await createOctoClient(token);
29+
30+
// Assert
31+
expect(result.request.endpoint.DEFAULTS.baseUrl).toBe('https://api.github.com');
32+
});
33+
34+
test('Creates app client to GitHub ES', async () => {
35+
// Arrange
36+
const enterpriseServer = 'https://github.enterprise.notgoingtowork';
37+
const token = '123456';
38+
39+
// Act
40+
const result = await createOctoClient(token, enterpriseServer);
41+
42+
// Assert
43+
expect(result.request.endpoint.DEFAULTS.baseUrl).toBe(enterpriseServer);
44+
});
45+
});
46+
47+
describe('Test createGithubAuth', () => {
48+
const mockedDecrypt = (decrypt as unknown) as jest.Mock;
49+
const mockedCreatAppAuth = (createAppAuth as unknown) as jest.Mock;
50+
const mockedDefaults = jest.spyOn(request, 'defaults');
51+
let mockedRequestInterface: MockProxy<RequestInterface>;
52+
53+
const installationId = 1;
54+
const authType = 'app';
55+
const token = '123456';
56+
const decryptedValue = 'decryptedValue';
57+
const b64 = Buffer.from(decryptedValue, 'binary').toString('base64');
58+
59+
beforeEach(() => {
60+
process.env.GITHUB_APP_ID = '1';
61+
process.env.GITHUB_APP_CLIENT_SECRET = 'client_secret';
62+
process.env.GITHUB_APP_KEY_BASE64 = 'base64';
63+
process.env.KMS_KEY_ID = 'key_id';
64+
process.env.ENVIRONMENT = 'dev';
65+
process.env.GITHUB_APP_CLIENT_ID = '1';
66+
});
67+
68+
test('Creates auth object for public GitHub', async () => {
69+
// Arrange
70+
const authOptions = {
71+
appId: parseInt(process.env.GITHUB_APP_ID as string),
72+
privateKey: 'decryptedValue',
73+
installationId,
74+
clientId: process.env.GITHUB_APP_CLIENT_ID,
75+
clientSecret: 'decryptedValue',
76+
};
77+
78+
mockedDecrypt.mockResolvedValueOnce(decryptedValue).mockResolvedValueOnce(b64);
79+
const mockedAuth = jest.fn();
80+
mockedAuth.mockResolvedValue({ token });
81+
mockedCreatAppAuth.mockImplementation((authOptions: StrategyOptions) => {
82+
return mockedAuth;
83+
});
84+
85+
// Act
86+
const result = await createGithubAuth(installationId, authType);
87+
88+
// Assert
89+
expect(mockedDecrypt).toBeCalledWith(
90+
process.env.GITHUB_APP_CLIENT_SECRET,
91+
process.env.KMS_KEY_ID,
92+
process.env.ENVIRONMENT,
93+
);
94+
expect(mockedDecrypt).toBeCalledWith(
95+
process.env.GITHUB_APP_KEY_BASE64,
96+
process.env.KMS_KEY_ID,
97+
process.env.ENVIRONMENT,
98+
);
99+
expect(mockedCreatAppAuth).toBeCalledTimes(1);
100+
expect(mockedCreatAppAuth).toBeCalledWith(authOptions);
101+
expect(mockedAuth).toBeCalledWith({ type: authType });
102+
expect(result.token).toBe(token);
103+
});
104+
105+
test('Creates auth object for Enterprise Server', async () => {
106+
// Arrange
107+
const githubServerUrl = 'https://github.enterprise.notgoingtowork';
108+
109+
mockedRequestInterface = mock<RequestInterface>();
110+
mockedDefaults.mockImplementation(() => {
111+
return mockedRequestInterface.defaults({ baseUrl: githubServerUrl });
112+
});
113+
114+
const authOptions = {
115+
appId: parseInt(process.env.GITHUB_APP_ID as string),
116+
privateKey: 'decryptedValue',
117+
installationId,
118+
clientId: process.env.GITHUB_APP_CLIENT_ID,
119+
clientSecret: 'decryptedValue',
120+
request: mockedRequestInterface.defaults({ baseUrl: githubServerUrl }),
121+
};
122+
123+
mockedDecrypt.mockResolvedValueOnce(decryptedValue).mockResolvedValueOnce(b64);
124+
const mockedAuth = jest.fn();
125+
mockedAuth.mockResolvedValue({ token });
126+
mockedCreatAppAuth.mockImplementation((authOptions: StrategyOptions) => {
127+
return mockedAuth;
128+
});
129+
130+
// Act
131+
const result = await createGithubAuth(installationId, authType, githubServerUrl);
132+
133+
// Assert
134+
expect(mockedDecrypt).toBeCalledWith(
135+
process.env.GITHUB_APP_CLIENT_SECRET,
136+
process.env.KMS_KEY_ID,
137+
process.env.ENVIRONMENT,
138+
);
139+
expect(mockedDecrypt).toBeCalledWith(
140+
process.env.GITHUB_APP_KEY_BASE64,
141+
process.env.KMS_KEY_ID,
142+
process.env.ENVIRONMENT,
143+
);
144+
expect(mockedCreatAppAuth).toBeCalledTimes(1);
145+
expect(mockedCreatAppAuth).toBeCalledWith(authOptions);
146+
expect(mockedAuth).toBeCalledWith({ type: authType });
147+
expect(result.token).toBe(token);
148+
});
149+
});

0 commit comments

Comments
 (0)