Skip to content

feat(ghes): Support for GitHub Enterprise Server #412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 14, 2021
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
1 change: 1 addition & 0 deletions .ci/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
1 change: 1 addition & 0 deletions .ci/build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
set -e

lambdaSrcDirs=("modules/runner-binaries-syncer/lambdas/runner-binaries-syncer" "modules/runners/lambdas/runners" "modules/webhook/lambdas/webhook")
repoRoot=$(dirname $(dirname $(realpath ${BASH_SOURCE[0]})))
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ example/*.secrets*.tfvars
*.gz
*.tgz
*.env
.vscode

**/coverage/*
139 changes: 76 additions & 63 deletions README.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ module "runners" {
lambda_zip = var.runners_lambda_zip
lambda_timeout_scale_up = var.runners_scale_up_lambda_timeout
lambda_timeout_scale_down = var.runners_scale_down_lambda_timeout
lambda_subnet_ids = var.lambda_subnet_ids
lambda_security_group_ids = var.lambda_security_group_ids
logging_retention_in_days = var.logging_retention_in_days
enable_cloudwatch_agent = var.enable_cloudwatch_agent
cloudwatch_config = var.cloudwatch_config
Expand All @@ -104,10 +106,13 @@ module "runners" {
userdata_template = var.userdata_template
userdata_pre_install = var.userdata_pre_install
userdata_post_install = var.userdata_post_install
key_name = var.key_name

create_service_linked_role_spot = var.create_service_linked_role_spot

runner_iam_role_managed_policy_arns = var.runner_iam_role_managed_policy_arns

ghes_url = var.ghes_url
}

module "runner_binaries" {
Expand Down
19 changes: 19 additions & 0 deletions modules/download-lambda/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions modules/runner-binaries-syncer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ No requirements.
| environment | A name that identifies the environment, used as prefix and for tagging. | `string` | n/a | yes |
| lambda\_s3\_bucket | S3 bucket from which to specify lambda functions. This is an alternative to providing local files directly. | `any` | `null` | no |
| lambda\_schedule\_expression | Scheduler expression for action runner binary syncer. | `string` | `"cron(27 * * * ? *)"` | no |
| 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 |
| 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 |
| lambda\_timeout | Time out of the lambda in seconds. | `number` | `300` | no |
| lambda\_zip | File location of the lambda zip file. | `string` | `null` | no |
| 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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"
"trailingComma": "all",
"semi": true,
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"lint": "yarn eslint --ext ts,tsx src",
"watch": "ts-node-dev --respawn --exit-child src/local.ts",
"build": "ncc build src/lambda.ts -o dist",
"dist": "yarn build && cd dist && zip ../runner-binaries-syncer.zip index.js"
"dist": "yarn build && cd dist && zip ../runner-binaries-syncer.zip index.js",
"format": "prettier --write \"**/*.ts\"",
"format-check": "prettier --check \"**/*.ts\""
},
"devDependencies": {
"@octokit/rest": "^18.0.12",
Expand Down
7 changes: 7 additions & 0 deletions modules/runner-binaries-syncer/runner-binaries-syncer.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ resource "aws_lambda_function" "syncer" {
GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = var.runner_allow_prerelease_binaries
}
}
dynamic "vpc_config" {
for_each = var.lambda_subnet_ids != null && var.lambda_security_group_ids != null ? [true] : []
content {
security_group_ids = var.lambda_security_group_ids
subnet_ids = var.lambda_subnet_ids
}
}

tags = var.tags
}
Expand Down
11 changes: 11 additions & 0 deletions modules/runner-binaries-syncer/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ variable "syncer_lambda_s3_object_version" {
default = null
}

variable "lambda_subnet_ids" {
description = "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`."
type = list(string)
default = []
}

variable "lambda_security_group_ids" {
description = "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`."
type = list(string)
default = []
}
4 changes: 4 additions & 0 deletions modules/runners/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,15 @@ No requirements.
| 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 |
| 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 |
| environment | A name that identifies the environment, used as prefix and for tagging. | `string` | n/a | yes |
| ghes\_url | GitHub Enterprise Server URL. DO NOT SET IF USING PUBLIC GITHUB | `string` | `null` | no |
| 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 |
| 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 |
| 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 |
| instance\_type | Default instance type for the action runner. | `string` | `"m5.large"` | no |
| key\_name | Key pair name | `string` | `null` | no |
| lambda\_s3\_bucket | S3 bucket from which to specify lambda functions. This is an alternative to providing local files directly. | `any` | `null` | no |
| 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 |
| 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 |
| lambda\_timeout\_scale\_down | Time out for the scale down lambda in seconds. | `number` | `60` | no |
| lambda\_timeout\_scale\_up | Time out for the scale up lambda in seconds. | `number` | `60` | no |
| lambda\_zip | File location of the lambda zip file. | `string` | `null` | no |
Expand Down
3 changes: 2 additions & 1 deletion modules/runners/lambdas/runners/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"
"trailingComma": "all",
"semi": true,
}
10 changes: 10 additions & 0 deletions modules/runners/lambdas/runners/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{ts,js,jsx}'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
17 changes: 12 additions & 5 deletions modules/runners/lambdas/runners/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@
"test": "NODE_ENV=test jest",
"watch": "ts-node-dev --respawn --exit-child src/local.ts",
"build": "ncc build src/lambda.ts -o dist",
"dist": "yarn build && cd dist && zip ../runners.zip index.js"
"dist": "yarn build && cd dist && zip ../runners.zip index.js",
"format": "prettier --write \"**/*.ts\"",
"format-check": "prettier --check \"**/*.ts\""
},
"devDependencies": {
"@types/aws-lambda": "^8.10.68",
"@types/express": "^4.17.9",
"@types/jest": "^26.0.19",
"@types/node": "^14.14.16",
"@vercel/ncc": "^0.26.1",
"aws-sdk": "^2.817.0",
"jest": "^26.6.3",
"jest-mock-extended": "^1.0.10",
"nock": "^13.0.5",
"ts-jest": "^26.4.4",
"ts-node-dev": "^1.1.1",
"typescript": "^4.1.3"
"ts-node-dev": "^1.1.1"
},
"dependencies": {
"@octokit/auth-app": "^2.10.5",
"@octokit/rest": "^18.0.12",
"@octokit/types": "^6.1.1",
"@types/aws-lambda": "^8.10.68",
"@types/express": "^4.17.9",
"@types/node": "^14.14.16",
"aws-sdk": "^2.817.0",
"cron-parser": "^2.18.0",
"moment": "^2.29.1",
"typescript": "^4.1.3",
"yn": "^4.0.0"
}
}
149 changes: 149 additions & 0 deletions modules/runners/lambdas/runners/src/scale-runners/gh-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { createOctoClient, createGithubAuth } from './gh-auth';
import nock from 'nock';
import { createAppAuth } from '@octokit/auth-app';
import { StrategyOptions } from '@octokit/auth-app/dist-types/types';
import { decrypt } from './kms';
import { RequestInterface } from '@octokit/types';
import { mock, MockProxy } from 'jest-mock-extended';
import { request } from '@octokit/request';

jest.mock('./kms');
jest.mock('@octokit/auth-app');

const cleanEnv = process.env;

beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
process.env = { ...cleanEnv };
nock.disableNetConnect();
});

describe('Test createGithubAuth', () => {
test('Creates app client to GitHub public', async () => {
// Arrange
const token = '123456';

// Act
const result = await createOctoClient(token);

// Assert
expect(result.request.endpoint.DEFAULTS.baseUrl).toBe('https://api.github.com');
});

test('Creates app client to GitHub ES', async () => {
// Arrange
const enterpriseServer = 'https://github.enterprise.notgoingtowork';
const token = '123456';

// Act
const result = await createOctoClient(token, enterpriseServer);

// Assert
expect(result.request.endpoint.DEFAULTS.baseUrl).toBe(enterpriseServer);
});
});

describe('Test createGithubAuth', () => {
const mockedDecrypt = (decrypt as unknown) as jest.Mock;
const mockedCreatAppAuth = (createAppAuth as unknown) as jest.Mock;
const mockedDefaults = jest.spyOn(request, 'defaults');
let mockedRequestInterface: MockProxy<RequestInterface>;

const installationId = 1;
const authType = 'app';
const token = '123456';
const decryptedValue = 'decryptedValue';
const b64 = Buffer.from(decryptedValue, 'binary').toString('base64');

beforeEach(() => {
process.env.GITHUB_APP_ID = '1';
process.env.GITHUB_APP_CLIENT_SECRET = 'client_secret';
process.env.GITHUB_APP_KEY_BASE64 = 'base64';
process.env.KMS_KEY_ID = 'key_id';
process.env.ENVIRONMENT = 'dev';
process.env.GITHUB_APP_CLIENT_ID = '1';
});

test('Creates auth object for public GitHub', async () => {
// Arrange
const authOptions = {
appId: parseInt(process.env.GITHUB_APP_ID as string),
privateKey: 'decryptedValue',
installationId,
clientId: process.env.GITHUB_APP_CLIENT_ID,
clientSecret: 'decryptedValue',
};

mockedDecrypt.mockResolvedValueOnce(decryptedValue).mockResolvedValueOnce(b64);
const mockedAuth = jest.fn();
mockedAuth.mockResolvedValue({ token });
mockedCreatAppAuth.mockImplementation((authOptions: StrategyOptions) => {
return mockedAuth;
});

// Act
const result = await createGithubAuth(installationId, authType);

// Assert
expect(mockedDecrypt).toBeCalledWith(
process.env.GITHUB_APP_CLIENT_SECRET,
process.env.KMS_KEY_ID,
process.env.ENVIRONMENT,
);
expect(mockedDecrypt).toBeCalledWith(
process.env.GITHUB_APP_KEY_BASE64,
process.env.KMS_KEY_ID,
process.env.ENVIRONMENT,
);
expect(mockedCreatAppAuth).toBeCalledTimes(1);
expect(mockedCreatAppAuth).toBeCalledWith(authOptions);
expect(mockedAuth).toBeCalledWith({ type: authType });
expect(result.token).toBe(token);
});

test('Creates auth object for Enterprise Server', async () => {
// Arrange
const githubServerUrl = 'https://github.enterprise.notgoingtowork';

mockedRequestInterface = mock<RequestInterface>();
mockedDefaults.mockImplementation(() => {
return mockedRequestInterface.defaults({ baseUrl: githubServerUrl });
});

const authOptions = {
appId: parseInt(process.env.GITHUB_APP_ID as string),
privateKey: 'decryptedValue',
installationId,
clientId: process.env.GITHUB_APP_CLIENT_ID,
clientSecret: 'decryptedValue',
request: mockedRequestInterface.defaults({ baseUrl: githubServerUrl }),
};

mockedDecrypt.mockResolvedValueOnce(decryptedValue).mockResolvedValueOnce(b64);
const mockedAuth = jest.fn();
mockedAuth.mockResolvedValue({ token });
mockedCreatAppAuth.mockImplementation((authOptions: StrategyOptions) => {
return mockedAuth;
});

// Act
const result = await createGithubAuth(installationId, authType, githubServerUrl);

// Assert
expect(mockedDecrypt).toBeCalledWith(
process.env.GITHUB_APP_CLIENT_SECRET,
process.env.KMS_KEY_ID,
process.env.ENVIRONMENT,
);
expect(mockedDecrypt).toBeCalledWith(
process.env.GITHUB_APP_KEY_BASE64,
process.env.KMS_KEY_ID,
process.env.ENVIRONMENT,
);
expect(mockedCreatAppAuth).toBeCalledTimes(1);
expect(mockedCreatAppAuth).toBeCalledWith(authOptions);
expect(mockedAuth).toBeCalledWith({ type: authType });
expect(result.token).toBe(token);
});
});
Loading