Skip to content

Commit 4c50957

Browse files
committed
feat: introduce github caching
1 parent 6f8ce01 commit 4c50957

File tree

5 files changed

+129
-35
lines changed

5 files changed

+129
-35
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: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,74 +22,106 @@ 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 {
26+
getClient,
27+
getAuthObject,
28+
getAuthConfig,
29+
createTokenCacheKey,
30+
createAuthCacheKey,
31+
createAuthConfigCacheKey
32+
} from './cache';
33+
import type { GithubAppConfig } from './types';
2534

2635
const logger = createChildLogger('gh-auth');
2736

2837
export async function createOctokitClient(token: string, ghesApiUrl = ''): Promise<Octokit> {
29-
const CustomOctokit = Octokit.plugin(throttling);
30-
const ocktokitOptions: OctokitOptions = {
31-
auth: token,
32-
};
33-
if (ghesApiUrl) {
34-
ocktokitOptions.baseUrl = ghesApiUrl;
35-
ocktokitOptions.previews = ['antiope'];
36-
}
38+
const cacheKey = createTokenCacheKey(token, ghesApiUrl);
3739

38-
return new CustomOctokit({
39-
...ocktokitOptions,
40-
userAgent: process.env.USER_AGENT || 'github-aws-runners',
41-
throttle: {
42-
onRateLimit: (retryAfter: number, options: Required<EndpointDefaults>) => {
43-
logger.warn(
44-
`GitHub rate limit: Request quota exhausted for request ${options.method} ${options.url}. Requested `,
45-
);
46-
},
47-
onSecondaryRateLimit: (retryAfter: number, options: Required<EndpointDefaults>) => {
48-
logger.warn(`GitHub rate limit: SecondaryRateLimit detected for request ${options.method} ${options.url}`);
40+
return getClient(cacheKey, async () => {
41+
const CustomOctokit = Octokit.plugin(throttling);
42+
const ocktokitOptions: OctokitOptions = {
43+
auth: token,
44+
};
45+
if (ghesApiUrl) {
46+
ocktokitOptions.baseUrl = ghesApiUrl;
47+
ocktokitOptions.previews = ['antiope'];
48+
}
49+
50+
return new CustomOctokit({
51+
...ocktokitOptions,
52+
userAgent: process.env.USER_AGENT || 'github-aws-runners',
53+
throttle: {
54+
onRateLimit: (retryAfter: number, options: Required<EndpointDefaults>) => {
55+
logger.warn(
56+
`GitHub rate limit: Request quota exhausted for request ${options.method} ${options.url}. Requested `,
57+
);
58+
},
59+
onSecondaryRateLimit: (retryAfter: number, options: Required<EndpointDefaults>) => {
60+
logger.warn(`GitHub rate limit: SecondaryRateLimit detected for request ${options.method} ${options.url}`);
61+
},
4962
},
50-
},
63+
});
5164
});
5265
}
5366

5467
export async function createGithubAppAuth(
5568
installationId: number | undefined,
5669
ghesApiUrl = '',
5770
): Promise<AppAuthentication> {
58-
const auth = await createAuth(installationId, ghesApiUrl);
59-
const appAuthOptions: AppAuthOptions = { type: 'app' };
60-
return auth(appAuthOptions);
71+
const cacheKey = createAuthCacheKey('app', installationId, ghesApiUrl);
72+
73+
return getAuthObject<AppAuthentication>(cacheKey, async () => {
74+
const auth = await createAuth(installationId, ghesApiUrl);
75+
const appAuthOptions: AppAuthOptions = { type: 'app' };
76+
return auth(appAuthOptions);
77+
});
6178
}
6279

6380
export async function createGithubInstallationAuth(
6481
installationId: number | undefined,
6582
ghesApiUrl = '',
6683
): Promise<InstallationAccessTokenAuthentication> {
67-
const auth = await createAuth(installationId, ghesApiUrl);
68-
const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId };
69-
return auth(installationAuthOptions);
84+
const cacheKey = createAuthCacheKey('installation', installationId, ghesApiUrl);
85+
86+
return getAuthObject<InstallationAccessTokenAuthentication>(cacheKey, async () => {
87+
const auth = await createAuth(installationId, ghesApiUrl);
88+
const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId };
89+
return auth(installationAuthOptions);
90+
});
7091
}
7192

7293
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(
94+
const configCacheKey = createAuthConfigCacheKey(ghesApiUrl);
95+
96+
const config = await getAuthConfig(configCacheKey, async (): Promise<GithubAppConfig> => {
97+
const appId = parseInt(await getParameter(process.env.PARAMETER_GITHUB_APP_ID_NAME));
98+
const privateKey = Buffer.from(
7799
await getParameter(process.env.PARAMETER_GITHUB_APP_KEY_BASE64_NAME),
78100
'base64',
79101
// replace literal \n characters with new lines to allow the key to be stored as a
80102
// single line variable. This logic should match how the GitHub Terraform provider
81103
// processes private keys to retain compatibility between the projects
82104
)
83105
.toString()
84-
.replace('/[\\n]/g', String.fromCharCode(10)),
106+
.replace('/[\\n]/g', String.fromCharCode(10));
107+
108+
return {
109+
appId,
110+
privateKey,
111+
};
112+
});
113+
114+
let authOptions: StrategyOptions = {
115+
appId: config.appId,
116+
privateKey: config.privateKey,
85117
};
86118
if (installationId) authOptions = { ...authOptions, installationId };
87119

88120
logger.debug(`GHES API URL: ${ghesApiUrl}`);
89121
if (ghesApiUrl) {
90122
authOptions.request = request.defaults({
91123
baseUrl: ghesApiUrl,
92-
});
124+
}) as RequestInterface;
93125
}
94126
return createAppAuth(authOptions);
95127
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Octokit } from '@octokit/rest';
2+
import type { GithubAppConfig } from './types';
3+
4+
const clients = new Map<string, Octokit>();
5+
const authObjects = new Map<string, any>();
6+
const authConfigs = new Map<string, GithubAppConfig>();
7+
8+
export function createTokenCacheKey(token: string, ghesApiUrl: string = '') {
9+
return `octokit-${token}-${ghesApiUrl}`;
10+
}
11+
12+
export function createAuthCacheKey(type: 'app' | 'installation', installationId?: number, ghesApiUrl: string = '') {
13+
const id = installationId || 'none';
14+
return `${type}-auth-${id}-${ghesApiUrl}`;
15+
}
16+
17+
export function createAuthConfigCacheKey(ghesApiUrl: string = '') {
18+
return `auth-config-${ghesApiUrl}`;
19+
}
20+
21+
export async function getClient(key: string, creator: () => Promise<Octokit>): Promise<Octokit> {
22+
if (clients.has(key)) {
23+
return clients.get(key)!;
24+
}
25+
26+
const client = await creator();
27+
clients.set(key, client);
28+
return client;
29+
}
30+
31+
export async function getAuthObject<T>(key: string, creator: () => Promise<T>) {
32+
if (authObjects.has(key)) {
33+
return authObjects.get(key) as T;
34+
}
35+
36+
const authObj = await creator();
37+
authObjects.set(key, authObj);
38+
return authObj;
39+
}
40+
41+
export async function getAuthConfig(key: string, creator: () => Promise<GithubAppConfig>) {
42+
if (authConfigs.has(key)) {
43+
return authConfigs.get(key)!;
44+
}
45+
46+
const config = await creator();
47+
authConfigs.set(key, config);
48+
return config;
49+
}
50+
51+
export function reset() {
52+
clients.clear();
53+
authObjects.clear();
54+
authConfigs.clear();
55+
}
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+
}

0 commit comments

Comments
 (0)