diff --git a/lambdas/functions/control-plane/src/github/auth.test.ts b/lambdas/functions/control-plane/src/github/auth.test.ts index 80b4314182..a0fb3194e7 100644 --- a/lambdas/functions/control-plane/src/github/auth.test.ts +++ b/lambdas/functions/control-plane/src/github/auth.test.ts @@ -1,10 +1,9 @@ -import { createAppAuth } from '@octokit/auth-app'; -import { StrategyOptions } from '@octokit/auth-app/dist-types/types'; +import { createAppAuth, type StrategyOptions } from '@octokit/auth-app'; import { request } from '@octokit/request'; import { RequestInterface, RequestParameters } from '@octokit/types'; import { getParameter } from '@aws-github-runner/aws-ssm-util'; import * as nock from 'nock'; - +import { reset } from './cache'; import { createGithubAppAuth, createOctokitClient } from './auth'; import { describe, it, expect, beforeEach, vi } from 'vitest'; @@ -29,6 +28,7 @@ const PARAMETER_GITHUB_APP_KEY_BASE64_NAME = `/actions-runner/${ENVIRONMENT}/git const mockedGet = vi.mocked(getParameter); beforeEach(() => { + reset(); // clear all caches before each test vi.resetModules(); vi.clearAllMocks(); process.env = { ...cleanEnv }; diff --git a/lambdas/functions/control-plane/src/github/auth.ts b/lambdas/functions/control-plane/src/github/auth.ts index d529cc81e8..9e68ef0e0c 100644 --- a/lambdas/functions/control-plane/src/github/auth.ts +++ b/lambdas/functions/control-plane/src/github/auth.ts @@ -22,6 +22,8 @@ import { throttling } from '@octokit/plugin-throttling'; import { createChildLogger } from '@aws-github-runner/aws-powertools-util'; import { getParameter } from '@aws-github-runner/aws-ssm-util'; import { EndpointDefaults } from '@octokit/types'; +import { getInstallationAuthObject, getAuthConfig, createAuthCacheKey, createAuthConfigCacheKey } from './cache'; +import type { GithubAppConfig } from './types'; const logger = createChildLogger('gh-auth'); @@ -64,16 +66,21 @@ export async function createGithubInstallationAuth( installationId: number | undefined, ghesApiUrl = '', ): Promise { - const auth = await createAuth(installationId, ghesApiUrl); - const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId }; - return auth(installationAuthOptions); + const cacheKey = createAuthCacheKey('installation', installationId, ghesApiUrl); + + return getInstallationAuthObject(cacheKey, async () => { + const auth = await createAuth(installationId, ghesApiUrl); + const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId }; + return auth(installationAuthOptions); + }); } async function createAuth(installationId: number | undefined, ghesApiUrl: string): Promise { - const appId = parseInt(await getParameter(process.env.PARAMETER_GITHUB_APP_ID_NAME)); - let authOptions: StrategyOptions = { - appId, - privateKey: Buffer.from( + const configCacheKey = createAuthConfigCacheKey(ghesApiUrl); + + const config = await getAuthConfig(configCacheKey, async (): Promise => { + const appId = parseInt(await getParameter(process.env.PARAMETER_GITHUB_APP_ID_NAME)); + const privateKey = Buffer.from( await getParameter(process.env.PARAMETER_GITHUB_APP_KEY_BASE64_NAME), 'base64', // replace literal \n characters with new lines to allow the key to be stored as a @@ -81,7 +88,17 @@ async function createAuth(installationId: number | undefined, ghesApiUrl: string // processes private keys to retain compatibility between the projects ) .toString() - .replace('/[\\n]/g', String.fromCharCode(10)), + .replace('/[\\n]/g', String.fromCharCode(10)); + + return { + appId, + privateKey, + }; + }); + + let authOptions: StrategyOptions = { + appId: config.appId, + privateKey: config.privateKey, }; if (installationId) authOptions = { ...authOptions, installationId }; @@ -89,7 +106,7 @@ async function createAuth(installationId: number | undefined, ghesApiUrl: string if (ghesApiUrl) { authOptions.request = request.defaults({ baseUrl: ghesApiUrl, - }); + }) as RequestInterface; } return createAppAuth(authOptions); } diff --git a/lambdas/functions/control-plane/src/github/cache.ts b/lambdas/functions/control-plane/src/github/cache.ts new file mode 100644 index 0000000000..a2b28abe74 --- /dev/null +++ b/lambdas/functions/control-plane/src/github/cache.ts @@ -0,0 +1,42 @@ +import type { InstallationAccessTokenAuthentication } from '@octokit/auth-app'; +import type { GithubAppConfig } from './types'; + +const installationAuthObjects = new Map(); +const authConfigs = new Map(); + +export function createAuthCacheKey(type: 'app' | 'installation', installationId?: number, ghesApiUrl: string = '') { + const id = installationId || 'none'; + return `${type}-auth-${id}-${ghesApiUrl}`; +} + +export function createAuthConfigCacheKey(ghesApiUrl: string = '') { + return `auth-config-${ghesApiUrl}`; +} + +export async function getInstallationAuthObject( + key: string, + creator: () => Promise, +): Promise { + if (installationAuthObjects.has(key)) { + return installationAuthObjects.get(key)!; + } + + const authObj = await creator(); + installationAuthObjects.set(key, authObj); + return authObj; +} + +export async function getAuthConfig(key: string, creator: () => Promise) { + if (authConfigs.has(key)) { + return authConfigs.get(key)!; + } + + const config = await creator(); + authConfigs.set(key, config); + return config; +} + +export function reset() { + installationAuthObjects.clear(); + authConfigs.clear(); +} diff --git a/lambdas/functions/control-plane/src/github/index.ts b/lambdas/functions/control-plane/src/github/index.ts new file mode 100644 index 0000000000..18273b71d3 --- /dev/null +++ b/lambdas/functions/control-plane/src/github/index.ts @@ -0,0 +1,2 @@ +export * from './cache'; +export * from './types'; diff --git a/lambdas/functions/control-plane/src/github/types.ts b/lambdas/functions/control-plane/src/github/types.ts new file mode 100644 index 0000000000..73a38b62c0 --- /dev/null +++ b/lambdas/functions/control-plane/src/github/types.ts @@ -0,0 +1,5 @@ +export interface GithubAppConfig { + appId: number; + privateKey: string; + installationId?: number; +} diff --git a/lambdas/functions/control-plane/src/local.ts b/lambdas/functions/control-plane/src/local.ts index 2166da58fd..0be0e60e88 100644 --- a/lambdas/functions/control-plane/src/local.ts +++ b/lambdas/functions/control-plane/src/local.ts @@ -7,7 +7,6 @@ const sqsEvent = { { messageId: 'e8d74d08-644e-42ca-bf82-a67daa6c4dad', receiptHandle: - // eslint-disable-next-line max-len 'AQEBCpLYzDEKq4aKSJyFQCkJduSKZef8SJVOperbYyNhXqqnpFG5k74WygVAJ4O0+9nybRyeOFThvITOaS21/jeHiI5fgaM9YKuI0oGYeWCIzPQsluW5CMDmtvqv1aA8sXQ5n2x0L9MJkzgdIHTC3YWBFLQ2AxSveOyIHwW+cHLIFCAcZlOaaf0YtaLfGHGkAC4IfycmaijV8NSlzYgDuxrC9sIsWJ0bSvk5iT4ru/R4+0cjm7qZtGlc04k9xk5Fu6A+wRxMaIyiFRY+Ya19ykcevQldidmEjEWvN6CRToLgclk=', body: { repositoryName: 'self-hosted',