Skip to content
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

feat: Setup wizard for GitHub integration #30

Merged
merged 6 commits into from Jun 4, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .projen/deps.json

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

3 changes: 3 additions & 0 deletions .projen/tasks.json

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

1 change: 1 addition & 0 deletions .projenrc.js
Expand Up @@ -17,6 +17,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
'esbuild', // for faster NodejsFunction bundling
'@octokit/core',
'@octokit/auth-app',
'@octokit/rest',
'aws-sdk',
'@aws-sdk/types',
],
Expand Down
15 changes: 15 additions & 0 deletions API.md
Expand Up @@ -924,6 +924,7 @@ Any object.
| <code><a href="#@cloudsnorkel/cdk-github-runners.Secrets.property.node">node</a></code> | <code>constructs.Node</code> | The tree node. |
| <code><a href="#@cloudsnorkel/cdk-github-runners.Secrets.property.github">github</a></code> | <code>aws-cdk-lib.aws_secretsmanager.Secret</code> | Authentication secret for GitHub containing either app details or personal authentication token. |
| <code><a href="#@cloudsnorkel/cdk-github-runners.Secrets.property.githubPrivateKey">githubPrivateKey</a></code> | <code>aws-cdk-lib.aws_secretsmanager.Secret</code> | GitHub app private key. Not needed when using personal authentication tokens. |
| <code><a href="#@cloudsnorkel/cdk-github-runners.Secrets.property.setup">setup</a></code> | <code>aws-cdk-lib.aws_secretsmanager.Secret</code> | Setup secret used to authenticate user for our setup wizard. |
| <code><a href="#@cloudsnorkel/cdk-github-runners.Secrets.property.webhook">webhook</a></code> | <code>aws-cdk-lib.aws_secretsmanager.Secret</code> | Webhook secret used to confirm events are coming from GitHub and nowhere else. |

---
Expand Down Expand Up @@ -971,6 +972,20 @@ This secret is meant to be edited by the user after being created. It is separat

---

##### `setup`<sup>Required</sup> <a name="setup" id="@cloudsnorkel/cdk-github-runners.Secrets.property.setup"></a>

```typescript
public readonly setup: Secret;
```

- *Type:* aws-cdk-lib.aws_secretsmanager.Secret

Setup secret used to authenticate user for our setup wizard.

Should be empty after setup has been completed.

---

##### `webhook`<sup>Required</sup> <a name="webhook" id="@cloudsnorkel/cdk-github-runners.Secrets.property.webhook"></a>

```typescript
Expand Down
12 changes: 6 additions & 6 deletions README.md
Expand Up @@ -10,22 +10,22 @@

Use this CDK construct to create ephemeral [self-hosted GitHub runners][1] on-demand inside your AWS account.

* Easy to configure GitHub integration
* Easy to configure GitHub integration with a web-based interface
* Customizable runners with decent defaults
* Supports multiple runner configurations controlled by labels
* Multiple runner configurations controlled by labels
* Everything fully hosted in your account

Self-hosted runners in AWS are useful when:

* You need easy access to internal resources in your actions
* You want to pre-install some software for your actions
* You want to provide some basic AWS API access ([aws-actions/configure-aws-credentials][2] has more security controls)
* You want to provide some basic AWS API access (but [aws-actions/configure-aws-credentials][2] has more security controls)

Ephemeral runners are the [recommended way by GitHub][14] for auto-scaling, and they make sure all jobs run with a clean image. Runners are started on-demand. You don't pay unless a job is running.
Ephemeral (or on-demand) runners are the [recommended way by GitHub][14] for auto-scaling, and they make sure all jobs run with a clean image. Runners are started on-demand. You don't pay unless a job is running.

## API

Documentation of available constructs and their interface is available on [Constructs Hub][13] in all supported programming languages.
The best way to browse API documentation is on [Constructs Hub][13]. It is available in all supported programming languages.

## Providers

Expand Down Expand Up @@ -72,7 +72,7 @@ You can also create your own provider by implementing `IRunnerProvider`.
4. Deploy your stack
5. Look for the status command output similar to `aws --region us-east-1 lambda invoke --function-name status-XYZ123 status.json`
6. Execute the status command (you may need to specify `--profile` too) and open the resulting `status.json` file
7. [Setup GitHub](SETUP_GITHUB.md) integration as an app or with personal access token
7. Open the URL in `github.setup.url` from `status.json` or [manually setup GitHub](SETUP_GITHUB.md) integration as an app or with personal access token
8. Run status command again to confirm `github.auth.status` and `github.webhook.status` are OK
9. Trigger a GitHub action that has a `self-hosted` label with `runs-on: [self-hosted, linux, codebuild]` or similar
10. If the action is not successful, see [troubleshooting](#Troubleshooting)
Expand Down
75 changes: 56 additions & 19 deletions SETUP_GITHUB.md
@@ -1,9 +1,23 @@
# Setup GitHub

Integration with GitHub can be done using an [app](#app-authentication) or [personal access token](#personal-access-token). Using an app allows more fine-grained access control. Personal access tokens are easier to set up but belong to a user instead of an organization.
Integration with GitHub can be done using an [app](#app-authentication) or [personal access token](#personal-access-token). Using an app allows more fine-grained access control. Using an app is easier with the setup wizard.

## App Authentication

### Setup Wizard

1. Open the URL in `github.setup.url` from `status.json`
2. If you want to create an app for your personal repositories, click the Create button under New Personal App
3. If you want to create an app for your organization:
1. Find the New Organization App section
2. Type in the organization name in organization slug (ORGANIZATION from https://github.com/ORGANIZATION/REPO)
3. Click the Create button
4. Follow the instructions on GitHub
5. When brought back to the setup wizard, click the install link
6. Install the new app on your desired repositories

### Manually

1. Decide if you want to create a personal app or an organization app
1. For a personal app use https://github.com/settings/apps/new
2. For an organization app use https://github.com/organizations/MY_ORG/settings/apps/new after replacing `MY_ORG` with your GitHub organization name
Expand All @@ -30,21 +44,44 @@ Integration with GitHub can be done using an [app](#app-authentication) or [pers

## Personal Access Token

1. Create a new token
1. Go to https://github.com/settings/tokens/new
2. Choose your expiration date (you will need to replace the token if it expires)
3. Under scopes select `repo`
4. Copy the generated token
2. Open the URL in `github.auth.secretUrl` from `status.json` and edit the secret value
1. If you're using a self-hosted GitHub instance, put its domain in `domain` (e.g. `github.mycompany.com`)
2. Put the generated token in `personalAuthToken`
3. Ignore all other values
3. Create a webhook
1. For organizations go to https://github.com/organizations/MY_ORG/settings/hooks after replacing `MY_ORG` with your GitHub organization name
2. For enterprise go to https://github.com/enterprises/MY_ENTERPRISE/settings/hooks after replacing `MY_ENTERPRISE` with your GitHub enterprise name
3. Otherwise, you can create one per repository in your repository settings under Webhooks
4. Configure the webhook:
1. For Webhook URL use the value of `github.webhook.url` from `status.json`
2. Open the URL in `github.webhook.secretUrl` from `status.json`, retrieve the secret value, and use it for webhook secret
3. Make sure content type is set to JSON
4. Select individual jobs and select only Workflow jobs
### Create Token

1. Go to https://github.com/settings/tokens/new
2. Choose your expiration date (you will need to replace the token if it expires)
3. Under scopes select `repo`
4. Copy the generated token

### Set Token

#### Setup Wizard

1. Open the URL in `github.setup.url` from `status.json`
2. Enter your personal access token under Using Personal Access Token
3. Click the Set button

#### Manually

1. Open the URL in `github.auth.secretUrl` from `status.json` and edit the secret value
2. If you're using a self-hosted GitHub instance, put its domain in `domain` (e.g. `github.mycompany.com`)
3. Put the generated token in `personalAuthToken`
4. Ignore all other values

### Setup Webhook

1. For organizations go to https://github.com/organizations/MY_ORG/settings/hooks after replacing `MY_ORG` with your GitHub organization name
2. For enterprise go to https://github.com/enterprises/MY_ENTERPRISE/settings/hooks after replacing `MY_ENTERPRISE` with your GitHub enterprise name
3. Otherwise, you can create one per repository in your repository settings under Webhooks
4. Configure the webhook:
1. For Webhook URL use the value of `github.webhook.url` from `status.json`
2. Open the URL in `github.webhook.secretUrl` from `status.json`, retrieve the secret value, and use it for webhook secret
3. Make sure content type is set to JSON
4. Select individual jobs and select only Workflow jobs

## Resetting Setup Wizard

If the setup wizard tells you setup has already been completed or if `github.setup.status` is completed, or if `github.setup.url` is empty:

1. Open the URL in `github.setup.secretUrl` from `status.json`
2. Edit the secret
3. Put a new random value in `token`
4. Run status function again to get the new URL
1 change: 1 addition & 0 deletions package.json

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

19 changes: 3 additions & 16 deletions src/lambdas/github.ts
@@ -1,9 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { createAppAuth } from '@octokit/auth-app';
import { Octokit } from '@octokit/core';
import * as AWS from 'aws-sdk';

const sm = new AWS.SecretsManager();
import { getSecretValue, getSecretJsonValue } from './helpers';

export function baseUrlFromDomain(domain: string): string {
if (domain == 'github.com') {
Expand All @@ -12,31 +10,20 @@ export function baseUrlFromDomain(domain: string): string {
return `https://${domain}/api/v3`;
}


export async function getOctokit(installationId?: string) {
if (!process.env.GITHUB_SECRET_ARN || !process.env.GITHUB_PRIVATE_KEY_SECRET_ARN) {
throw new Error('Missing environment variables');
}

const secret = await sm.getSecretValue({
SecretId: process.env.GITHUB_SECRET_ARN,
}).promise();

if (!secret.SecretString) {
throw new Error(`No secret string in ${process.env.GITHUB_SECRET_ARN}`);
}

const githubSecrets = JSON.parse(secret.SecretString);
const githubSecrets = await getSecretJsonValue(process.env.GITHUB_SECRET_ARN);

let baseUrl = baseUrlFromDomain(githubSecrets.domain);

let token;
if (githubSecrets.personalAuthToken) {
token = githubSecrets.personalAuthToken;
} else {
const privateKey = (await sm.getSecretValue({
SecretId: process.env.GITHUB_PRIVATE_KEY_SECRET_ARN,
}).promise()).SecretString;
const privateKey = await getSecretValue(process.env.GITHUB_PRIVATE_KEY_SECRET_ARN);

const appOctokit = new Octokit({
baseUrl,
Expand Down
30 changes: 30 additions & 0 deletions src/lambdas/helpers.ts
@@ -0,0 +1,30 @@
/* eslint-disable import/no-extraneous-dependencies */
import * as AWS from 'aws-sdk';

const sm = new AWS.SecretsManager();

export async function getSecretValue(arn: string | undefined) {
if (!arn) {
throw new Error('Missing secret ARN');
}

const secret = await sm.getSecretValue({ SecretId: arn }).promise();

if (!secret.SecretString) {
throw new Error(`No SecretString in ${arn}`);
}

return secret.SecretString;
}

export async function getSecretJsonValue(arn: string | undefined) {
return JSON.parse(await getSecretValue(arn));
}

export async function updateSecretValue(arn: string | undefined, value: string) {
if (!arn) {
throw new Error('Missing secret ARN');
}

await sm.updateSecret({ SecretId: arn, SecretString: value }).promise();
}