Skip to content

Commit 7e9cae3

Browse files
committed
feat: introduce github caching
1 parent c29838c commit 7e9cae3

File tree

7 files changed

+79
-14
lines changed

7 files changed

+79
-14
lines changed

lambdas/functions/control-plane/src/github/auth.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { createAppAuth } from '@octokit/auth-app';
2-
import { StrategyOptions } from '@octokit/auth-app/dist-types/types';
1+
import { createAppAuth, type StrategyOptions } from '@octokit/auth-app';
32
import { request } from '@octokit/request';
43
import { RequestInterface, RequestParameters } from '@octokit/types';
54
import { getParameter } from '@aws-github-runner/aws-ssm-util';
65
import * as nock from 'nock';
7-
6+
import { reset } from './cache';
87
import { createGithubAppAuth, createOctokitClient } from './auth';
98
import { describe, it, expect, beforeEach, vi } from 'vitest';
109

@@ -29,6 +28,7 @@ const PARAMETER_GITHUB_APP_KEY_BASE64_NAME = `/actions-runner/${ENVIRONMENT}/git
2928
const mockedGet = vi.mocked(getParameter);
3029

3130
beforeEach(() => {
31+
reset(); // clear all caches before each test
3232
vi.resetModules();
3333
vi.clearAllMocks();
3434
process.env = { ...cleanEnv };

lambdas/functions/control-plane/src/github/auth.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { throttling } from '@octokit/plugin-throttling';
2222
import { createChildLogger } from '@aws-github-runner/aws-powertools-util';
2323
import { getParameter } from '@aws-github-runner/aws-ssm-util';
2424
import { EndpointDefaults } from '@octokit/types';
25+
import { getInstallationAuthObject, getAuthConfig, createAuthCacheKey, createAuthConfigCacheKey } from './cache';
26+
import type { GithubAppConfig } from './types';
2527

2628
const logger = createChildLogger('gh-auth');
2729

@@ -64,32 +66,47 @@ export async function createGithubInstallationAuth(
6466
installationId: number | undefined,
6567
ghesApiUrl = '',
6668
): Promise<InstallationAccessTokenAuthentication> {
67-
const auth = await createAuth(installationId, ghesApiUrl);
68-
const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId };
69-
return auth(installationAuthOptions);
69+
const cacheKey = createAuthCacheKey('installation', installationId, ghesApiUrl);
70+
71+
return getInstallationAuthObject(cacheKey, async () => {
72+
const auth = await createAuth(installationId, ghesApiUrl);
73+
const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId };
74+
return auth(installationAuthOptions);
75+
});
7076
}
7177

7278
async function createAuth(installationId: number | undefined, ghesApiUrl: string): Promise<AuthInterface> {
73-
const appId = parseInt(await getParameter(process.env.PARAMETER_GITHUB_APP_ID_NAME));
74-
let authOptions: StrategyOptions = {
75-
appId,
76-
privateKey: Buffer.from(
79+
const configCacheKey = createAuthConfigCacheKey(ghesApiUrl);
80+
81+
const config = await getAuthConfig(configCacheKey, async (): Promise<GithubAppConfig> => {
82+
const appId = parseInt(await getParameter(process.env.PARAMETER_GITHUB_APP_ID_NAME));
83+
const privateKey = Buffer.from(
7784
await getParameter(process.env.PARAMETER_GITHUB_APP_KEY_BASE64_NAME),
7885
'base64',
7986
// replace literal \n characters with new lines to allow the key to be stored as a
8087
// single line variable. This logic should match how the GitHub Terraform provider
8188
// processes private keys to retain compatibility between the projects
8289
)
8390
.toString()
84-
.replace('/[\\n]/g', String.fromCharCode(10)),
91+
.replace('/[\\n]/g', String.fromCharCode(10));
92+
93+
return {
94+
appId,
95+
privateKey,
96+
};
97+
});
98+
99+
let authOptions: StrategyOptions = {
100+
appId: config.appId,
101+
privateKey: config.privateKey,
85102
};
86103
if (installationId) authOptions = { ...authOptions, installationId };
87104

88105
logger.debug(`GHES API URL: ${ghesApiUrl}`);
89106
if (ghesApiUrl) {
90107
authOptions.request = request.defaults({
91108
baseUrl: ghesApiUrl,
92-
});
109+
}) as RequestInterface;
93110
}
94111
return createAppAuth(authOptions);
95112
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { InstallationAccessTokenAuthentication } from '@octokit/auth-app';
2+
import type { GithubAppConfig } from './types';
3+
4+
const installationAuthObjects = new Map<string, InstallationAccessTokenAuthentication>();
5+
const authConfigs = new Map<string, GithubAppConfig>();
6+
7+
export function createAuthCacheKey(type: 'app' | 'installation', installationId?: number, ghesApiUrl: string = '') {
8+
const id = installationId || 'none';
9+
return `${type}-auth-${id}-${ghesApiUrl}`;
10+
}
11+
12+
export function createAuthConfigCacheKey(ghesApiUrl: string = '') {
13+
return `auth-config-${ghesApiUrl}`;
14+
}
15+
16+
export async function getInstallationAuthObject(
17+
key: string,
18+
creator: () => Promise<InstallationAccessTokenAuthentication>,
19+
): Promise<InstallationAccessTokenAuthentication> {
20+
if (installationAuthObjects.has(key)) {
21+
return installationAuthObjects.get(key)!;
22+
}
23+
24+
const authObj = await creator();
25+
installationAuthObjects.set(key, authObj);
26+
return authObj;
27+
}
28+
29+
export async function getAuthConfig(key: string, creator: () => Promise<GithubAppConfig>) {
30+
if (authConfigs.has(key)) {
31+
return authConfigs.get(key)!;
32+
}
33+
34+
const config = await creator();
35+
authConfigs.set(key, config);
36+
return config;
37+
}
38+
39+
export function reset() {
40+
installationAuthObjects.clear();
41+
authConfigs.clear();
42+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cache';
2+
export * from './types';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface GithubAppConfig {
2+
appId: number;
3+
privateKey: string;
4+
installationId?: number;
5+
}

lambdas/functions/control-plane/src/local.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const sqsEvent = {
77
{
88
messageId: 'e8d74d08-644e-42ca-bf82-a67daa6c4dad',
99
receiptHandle:
10-
// eslint-disable-next-line max-len
1110
'AQEBCpLYzDEKq4aKSJyFQCkJduSKZef8SJVOperbYyNhXqqnpFG5k74WygVAJ4O0+9nybRyeOFThvITOaS21/jeHiI5fgaM9YKuI0oGYeWCIzPQsluW5CMDmtvqv1aA8sXQ5n2x0L9MJkzgdIHTC3YWBFLQ2AxSveOyIHwW+cHLIFCAcZlOaaf0YtaLfGHGkAC4IfycmaijV8NSlzYgDuxrC9sIsWJ0bSvk5iT4ru/R4+0cjm7qZtGlc04k9xk5Fu6A+wRxMaIyiFRY+Ya19ykcevQldidmEjEWvN6CRToLgclk=',
1211
body: {
1312
repositoryName: 'self-hosted',

modules/multi-runner/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ module "multi-runner" {
171171
| <a name="input_runners_ssm_housekeeper"></a> [runners\_ssm\_housekeeper](#input\_runners\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.<br/><br/> `schedule_expression`: is used to configure the schedule for the lambda.<br/> `enabled`: enable or disable the lambda trigger via the EventBridge.<br/> `lambda_memory_size`: lambda memory size limit.<br/> `lambda_timeout`: timeout for the lambda in seconds.<br/> `config`: configuration for the lambda function. Token path will be read by default from the module. | <pre>object({<br/> schedule_expression = optional(string, "rate(1 day)")<br/> enabled = optional(bool, true)<br/> lambda_memory_size = optional(number, 512)<br/> lambda_timeout = optional(number, 60)<br/> config = object({<br/> tokenPath = optional(string)<br/> minimumDaysOld = optional(number, 1)<br/> dryRun = optional(bool, false)<br/> })<br/> })</pre> | <pre>{<br/> "config": {}<br/>}</pre> | no |
172172
| <a name="input_scale_down_lambda_memory_size"></a> [scale\_down\_lambda\_memory\_size](#input\_scale\_down\_lambda\_memory\_size) | Memory size limit in MB for scale down. | `number` | `512` | no |
173173
| <a name="input_scale_up_lambda_memory_size"></a> [scale\_up\_lambda\_memory\_size](#input\_scale\_up\_lambda\_memory\_size) | Memory size limit in MB for scale\_up lambda. | `number` | `512` | no |
174-
| <a name="input_ssm_paths"></a> [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secreets. | <pre>object({<br/> root = optional(string, "github-action-runners")<br/> app = optional(string, "app")<br/> runners = optional(string, "runners")<br/> webhook = optional(string, "webhook")<br/> })</pre> | `{}` | no |
174+
| <a name="input_ssm_paths"></a> [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. | <pre>object({<br/> root = optional(string, "github-action-runners")<br/> app = optional(string, "app")<br/> runners = optional(string, "runners")<br/> webhook = optional(string, "webhook")<br/> })</pre> | `{}` | no |
175175
| <a name="input_state_event_rule_binaries_syncer"></a> [state\_event\_rule\_binaries\_syncer](#input\_state\_event\_rule\_binaries\_syncer) | Option to disable EventBridge Lambda trigger for the binary syncer, useful to stop automatic updates of binary distribution | `string` | `"ENABLED"` | no |
176176
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_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)` | n/a | yes |
177177
| <a name="input_syncer_lambda_s3_key"></a> [syncer\_lambda\_s3\_key](#input\_syncer\_lambda\_s3\_key) | S3 key for syncer lambda function. Required if using S3 bucket to specify lambdas. | `string` | `null` | no |

0 commit comments

Comments
 (0)