diff --git a/lambdas/functions/control-plane/package.json b/lambdas/functions/control-plane/package.json index 8f978c0d17..dba41a20ef 100644 --- a/lambdas/functions/control-plane/package.json +++ b/lambdas/functions/control-plane/package.json @@ -33,6 +33,7 @@ "@aws-github-runner/aws-powertools-util": "*", "@aws-github-runner/aws-ssm-util": "*", "@aws-lambda-powertools/parameters": "^2.31.0", + "@aws-sdk/client-dynamodb": "^3.1009.0", "@aws-sdk/client-ec2": "^3.1009.0", "@aws-sdk/client-sqs": "^3.1009.0", "@middy/core": "^6.4.5", diff --git a/lambdas/functions/control-plane/src/github/auth.ts b/lambdas/functions/control-plane/src/github/auth.ts index 9a572c48a8..9e72bfa9ed 100644 --- a/lambdas/functions/control-plane/src/github/auth.ts +++ b/lambdas/functions/control-plane/src/github/auth.ts @@ -1,6 +1,7 @@ import { createAppAuth, type AppAuthentication, type InstallationAccessTokenAuthentication } from '@octokit/auth-app'; import type { OctokitOptions } from '@octokit/core'; import type { RequestInterface } from '@octokit/types'; +import { getCachedInstallationToken } from './token-cache'; // Define types that are not directly exported type AppAuthOptions = { type: 'app' }; @@ -76,6 +77,26 @@ export async function createGithubAppAuth( export async function createGithubInstallationAuth( installationId: number | undefined, ghesApiUrl = '', +): Promise { + if (installationId && process.env.INSTALLATION_TOKEN_TABLE_NAME) { + const cached = await getCachedInstallationToken(installationId, () => + mintInstallationToken(installationId, ghesApiUrl).then((r) => ({ token: r.token, expiresAt: r.expiresAt })), + ); + return { + type: 'token', + tokenType: 'installation', + token: cached.token, + expiresAt: cached.expiresAt.toISOString(), + installationId, + } as InstallationAccessTokenAuthentication; + } + + return mintInstallationToken(installationId, ghesApiUrl); +} + +async function mintInstallationToken( + installationId: number | undefined, + ghesApiUrl: string, ): Promise { const auth = await createAuth(installationId, ghesApiUrl); const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId }; diff --git a/lambdas/functions/control-plane/src/github/token-cache.test.ts b/lambdas/functions/control-plane/src/github/token-cache.test.ts new file mode 100644 index 0000000000..a8518783f3 --- /dev/null +++ b/lambdas/functions/control-plane/src/github/token-cache.test.ts @@ -0,0 +1,207 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +const { mockSend } = vi.hoisted(() => ({ mockSend: vi.fn() })); + +vi.mock('@aws-sdk/client-dynamodb', async () => { + const actual = await vi.importActual('@aws-sdk/client-dynamodb'); + class MockDynamoDBClient { + send = mockSend; + } + return { + ...actual, + DynamoDBClient: MockDynamoDBClient, + }; +}); + +import { + ConditionalCheckFailedException, + GetItemCommand, + PutItemCommand, + UpdateItemCommand, +} from '@aws-sdk/client-dynamodb'; +import { getCachedInstallationToken } from './token-cache'; + +const installationId = 138041; + +beforeEach(() => { + mockSend.mockReset(); + process.env.INSTALLATION_TOKEN_TABLE_NAME = 'test-installation-tokens'; +}); + +function freshTokenItem(expiresAtMs: number) { + return { + Item: { + installation_id: { N: String(installationId) }, + token: { S: 'cached-token-abc' }, + expires_at_ms: { N: String(expiresAtMs) }, + }, + }; +} + +describe('getCachedInstallationToken', () => { + it('returns cached token without calling mint when token is fresh', async () => { + const farFuture = Date.now() + 30 * 60 * 1000; + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) return freshTokenItem(farFuture); + throw new Error('unexpected command: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + const mint = vi.fn(); + + const result = await getCachedInstallationToken(installationId, mint); + + expect(result.token).toBe('cached-token-abc'); + expect(mint).not.toHaveBeenCalled(); + }); + + it('refreshes ahead and mints when cached token is approaching expiry', async () => { + const expiringSoon = Date.now() + 5 * 60 * 1000; + const calls: string[] = []; + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) return freshTokenItem(expiringSoon); + if (cmd instanceof UpdateItemCommand) { + calls.push('lock-acquired'); + return {}; + } + if (cmd instanceof PutItemCommand) { + calls.push('cache-write'); + return {}; + } + throw new Error('unexpected: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + const mint = vi.fn().mockResolvedValue({ + token: 'fresh-token', + expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }); + + const result = await getCachedInstallationToken(installationId, mint); + + expect(result.token).toBe('fresh-token'); + expect(mint).toHaveBeenCalledTimes(1); + expect(calls).toEqual(['lock-acquired', 'cache-write']); + }); + + it('returns cached token when refresh-ahead lock is held by another Lambda', async () => { + const expiringSoon = Date.now() + 5 * 60 * 1000; + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) return freshTokenItem(expiringSoon); + if (cmd instanceof UpdateItemCommand) { + throw new ConditionalCheckFailedException({ $metadata: {}, message: 'lock taken' }); + } + throw new Error('unexpected: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + const mint = vi.fn(); + + const result = await getCachedInstallationToken(installationId, mint); + + expect(result.token).toBe('cached-token-abc'); + expect(mint).not.toHaveBeenCalled(); + }); + + it('mints when cache is empty and we win the lock', async () => { + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) return { Item: undefined }; + if (cmd instanceof UpdateItemCommand) return {}; + if (cmd instanceof PutItemCommand) return {}; + throw new Error('unexpected: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + const mint = vi.fn().mockResolvedValue({ + token: 'minted', + expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }); + + const result = await getCachedInstallationToken(installationId, mint); + + expect(result.token).toBe('minted'); + expect(mint).toHaveBeenCalledTimes(1); + }); + + it('on cold-cache miss + lost lock: waits, re-reads, returns new cached token', async () => { + let getCalls = 0; + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) { + getCalls++; + if (getCalls === 1) return { Item: undefined }; + return freshTokenItem(Date.now() + 60 * 60 * 1000); + } + if (cmd instanceof UpdateItemCommand) { + throw new ConditionalCheckFailedException({ $metadata: {}, message: 'lock taken' }); + } + throw new Error('unexpected: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + const mint = vi.fn(); + + const result = await getCachedInstallationToken(installationId, mint); + + expect(result.token).toBe('cached-token-abc'); + expect(mint).not.toHaveBeenCalled(); + expect(getCalls).toBe(2); + }); + + it('on mint failure does not write to cache (lock expires naturally)', async () => { + const calls: string[] = []; + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) return { Item: undefined }; + if (cmd instanceof UpdateItemCommand) { + calls.push('lock-acquired'); + return {}; + } + if (cmd instanceof PutItemCommand) { + calls.push('SHOULD-NOT-WRITE'); + return {}; + } + throw new Error('unexpected: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + const error = Object.assign(new Error('Not Found'), { status: 404 }); + const mint = vi.fn().mockRejectedValue(error); + + await expect(getCachedInstallationToken(installationId, mint)).rejects.toMatchObject({ + status: 404, + }); + expect(calls).toEqual(['lock-acquired']); + }); + + it('does not acquire lock when a valid cached token already exists (race prevention)', async () => { + const freshExpiry = Date.now() + 50 * 60 * 1000; + let getCalls = 0; + mockSend.mockImplementation(async (cmd: unknown) => { + if (cmd instanceof GetItemCommand) { + getCalls++; + if (getCalls === 1) { + // First read: stale — no token + return { Item: { installation_id: { N: String(installationId) } } }; + } + // Second read: sees fresh token written by another Lambda + return freshTokenItem(freshExpiry); + } + if (cmd instanceof UpdateItemCommand) { + // DDB rejects because expires_at_ms > refreshAt + throw new ConditionalCheckFailedException({ + message: 'Condition not met', + $metadata: {}, + }); + } + throw new Error('unexpected: ' + (cmd as { constructor: { name: string } }).constructor.name); + }); + + const mint = vi.fn(); + const result = await getCachedInstallationToken(installationId, mint); + + expect(mint).not.toHaveBeenCalled(); + expect(result.token).toBe('cached-token-abc'); + expect(getCalls).toBe(2); + }); + + it('mints directly when INSTALLATION_TOKEN_TABLE_NAME is not set (cache disabled)', async () => { + delete process.env.INSTALLATION_TOKEN_TABLE_NAME; + const mint = vi.fn().mockResolvedValue({ + token: 'direct-mint', + expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }); + + const result = await getCachedInstallationToken(installationId, mint); + + expect(result.token).toBe('direct-mint'); + expect(mint).toHaveBeenCalledTimes(1); + expect(mockSend).not.toHaveBeenCalled(); + }); +}); diff --git a/lambdas/functions/control-plane/src/github/token-cache.ts b/lambdas/functions/control-plane/src/github/token-cache.ts new file mode 100644 index 0000000000..b19041f64f --- /dev/null +++ b/lambdas/functions/control-plane/src/github/token-cache.ts @@ -0,0 +1,227 @@ +import { + ConditionalCheckFailedException, + DynamoDBClient, + GetItemCommand, + PutItemCommand, + UpdateItemCommand, +} from '@aws-sdk/client-dynamodb'; +import { createChildLogger } from '@aws-github-runner/aws-powertools-util'; + +const logger = createChildLogger('installation-token-cache'); + +// Refresh token when it has less than this much life left. GitHub tokens are +// valid for 60 minutes; refreshing at 50 min left = 10 min before expiry +// provides buffer for clock skew, in-flight requests, and any temporarily +// failing refresh attempt. +const REFRESH_AHEAD_MS = 10 * 60 * 1000; + +// How long the mint-in-progress lock lives if the holder crashes. Must be +// > Octokit HTTP timeout so a slow successful mint doesn't race a duplicate. +const LOCK_TTL_MS = 60 * 1000; + +// Token lifetime written to the cache. GitHub tokens are valid for ~60 min; +// we record 58 to provide a consumer-side safety margin. +const TOKEN_TTL_MS = 58 * 60 * 1000; + +// Wait this long for the lock holder to finish on a cache miss before +// re-reading. Jittered to avoid thundering herd on the cache re-read. +const COLD_MISS_WAIT_MIN_MS = 200; +const COLD_MISS_WAIT_MAX_MS = 1000; + +const ddb = new DynamoDBClient({}); + +export interface CachedInstallationToken { + token: string; + expiresAt: Date; +} + +interface CacheEntry { + token?: string; + expiresAtMs?: number; + lockUntilMs?: number; +} + +/** + * Returns a valid installation access token, using the DynamoDB-backed cache + * when possible. The token-mint function is provided by the caller — this + * module is concerned only with caching, locking, and refresh-ahead. + * + * Three behavioural cases: + * A. Cache hit, well before expiry: return cached token, no GitHub call. + * B. Cache hit, approaching expiry (within REFRESH_AHEAD_MS): + * one Lambda wins a refresh lock and mints synchronously; others + * return the still-valid cached token without waiting. + * C. Cache miss / expired: one Lambda wins the lock and mints; others + * sleep briefly and re-read the cache, falling back to a direct mint + * only as a last resort if the winner failed. + * + * On mint failure, the lock is intentionally NOT released so it expires + * naturally after LOCK_TTL_MS. This caps mint attempts at one per + * LOCK_TTL_MS during sustained upstream failures. + */ +export async function getCachedInstallationToken( + installationId: number, + mintToken: () => Promise<{ token: string; expiresAt: string }>, +): Promise { + const tableName = process.env.INSTALLATION_TOKEN_TABLE_NAME; + if (!tableName) { + // Cache disabled — mint directly. + const minted = await mintToken(); + return { token: minted.token, expiresAt: new Date(minted.expiresAt) }; + } + + const now = Date.now(); + const cached = await readCacheEntry(tableName, installationId); + + // Case A: fresh cache hit, well before expiry → use it. + if (cached?.token && cached.expiresAtMs && cached.expiresAtMs > now + REFRESH_AHEAD_MS) { + logger.debug('Installation token cache hit', { installationId }); + return { token: cached.token, expiresAt: new Date(cached.expiresAtMs) }; + } + + // Case B: still valid but approaching expiry → refresh-ahead. + if (cached?.token && cached.expiresAtMs && cached.expiresAtMs > now) { + const acquired = await tryAcquireRefreshLock(tableName, installationId, now, cached); + if (acquired) { + logger.info('Refreshing installation token (refresh-ahead)', { installationId }); + return await mintAndStore(tableName, installationId, mintToken); + } else { + logger.debug('Another Lambda is refreshing; using cached token', { installationId }); + return { token: cached.token, expiresAt: new Date(cached.expiresAtMs) }; + } + } + + // Case C: cache miss or fully expired → must mint, blocking. Single-flight. + const acquired = await tryAcquireRefreshLock(tableName, installationId, now, cached); + if (acquired) { + logger.info('Minting installation token (cold cache)', { installationId }); + return await mintAndStore(tableName, installationId, mintToken); + } + + // Lock is held by another Lambda — wait briefly and re-read. + const jitter = COLD_MISS_WAIT_MIN_MS + Math.random() * (COLD_MISS_WAIT_MAX_MS - COLD_MISS_WAIT_MIN_MS); + await sleep(jitter); + const retried = await readCacheEntry(tableName, installationId); + if (retried?.token && retried.expiresAtMs && retried.expiresAtMs > Date.now()) { + logger.debug('Installation token populated by lock holder', { installationId }); + return { token: retried.token, expiresAt: new Date(retried.expiresAtMs) }; + } + + // Winner finished and either failed or cache is stale. Mint directly as a + // last resort — accepts the rare double-mint to ensure forward progress. + logger.warn('Lock holder did not populate cache; minting fallback', { installationId }); + return await mintAndStore(tableName, installationId, mintToken); +} + +async function readCacheEntry(tableName: string, installationId: number): Promise { + try { + const out = await ddb.send( + new GetItemCommand({ + TableName: tableName, + Key: { installation_id: { N: String(installationId) } }, + ConsistentRead: true, + }), + ); + if (!out.Item) return undefined; + return { + token: out.Item.token?.S, + expiresAtMs: out.Item.expires_at_ms?.N ? Number(out.Item.expires_at_ms.N) : undefined, + lockUntilMs: out.Item.lock_until_ms?.N ? Number(out.Item.lock_until_ms.N) : undefined, + }; + } catch (e) { + logger.warn('Token cache read failed; falling through to mint', { installationId, error: e }); + return undefined; + } +} + +/** + * Atomically acquire the mint lock by setting `lock_until_ms` only if no + * other lock is currently active. Returns true if we won the race. + */ +async function tryAcquireRefreshLock( + tableName: string, + installationId: number, + nowMs: number, + current: CacheEntry | undefined, +): Promise { + const lockUntil = nowMs + LOCK_TTL_MS; + try { + await ddb.send( + new UpdateItemCommand({ + TableName: tableName, + Key: { installation_id: { N: String(installationId) } }, + UpdateExpression: 'SET lock_until_ms = :lockUntil, #ttl = :ttl', + // Acquire if: + // 1. No item exists, OR no lock, OR current lock expired + // AND + // 2. No valid token exists (or token is within the refresh-ahead window) + ConditionExpression: + '(attribute_not_exists(installation_id) OR attribute_not_exists(lock_until_ms) OR lock_until_ms < :now)' + + ' AND ' + + '(attribute_not_exists(expires_at_ms) OR expires_at_ms < :refreshAt)', + ExpressionAttributeNames: { + '#ttl': 'ttl', + }, + ExpressionAttributeValues: { + ':lockUntil': { N: String(lockUntil) }, + ':now': { N: String(nowMs) }, + ':refreshAt': { N: String(nowMs + REFRESH_AHEAD_MS) }, + ':ttl': { N: String(Math.floor(lockUntil / 1000) + 600) }, + }, + }), + ); + return true; + } catch (e) { + if (e instanceof ConditionalCheckFailedException) { + return false; + } + logger.warn('Lock acquire failed unexpectedly; falling through', { + installationId, + error: e, + hadCachedToken: Boolean(current?.token), + }); + return false; + } +} + +/** + * Calls the supplied `mintToken` function, writes the result to DDB, and + * releases the lock. On failure does NOT release the lock — natural backoff + * via the lock TTL prevents thundering herd against a struggling upstream. + */ +async function mintAndStore( + tableName: string, + installationId: number, + mintToken: () => Promise<{ token: string; expiresAt: string }>, +): Promise { + const minted = await mintToken(); + const expiresAtMs = Math.min(new Date(minted.expiresAt).getTime(), Date.now() + TOKEN_TTL_MS); + + try { + await ddb.send( + new PutItemCommand({ + TableName: tableName, + Item: { + installation_id: { N: String(installationId) }, + token: { S: minted.token }, + expires_at_ms: { N: String(expiresAtMs) }, + // DynamoDB TTL operates on epoch seconds; let DDB clean up old + // entries automatically a few minutes after expiry. + ttl: { N: String(Math.floor(expiresAtMs / 1000) + 600) }, + // lock_until_ms is intentionally left unset — clears the lock. + }, + }), + ); + } catch (e) { + logger.warn('Token cache write failed; token still returned to caller', { + installationId, + error: e, + }); + } + + return { token: minted.token, expiresAt: new Date(expiresAtMs) }; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/lambdas/yarn.lock b/lambdas/yarn.lock index 9218463b05..4824464c13 100644 --- a/lambdas/yarn.lock +++ b/lambdas/yarn.lock @@ -149,6 +149,7 @@ __metadata: "@aws-github-runner/aws-powertools-util": "npm:*" "@aws-github-runner/aws-ssm-util": "npm:*" "@aws-lambda-powertools/parameters": "npm:^2.31.0" + "@aws-sdk/client-dynamodb": "npm:^3.1009.0" "@aws-sdk/client-ec2": "npm:^3.1009.0" "@aws-sdk/client-sqs": "npm:^3.1009.0" "@aws-sdk/types": "npm:^3.973.6" @@ -319,6 +320,26 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-dynamodb@npm:^3.1009.0": + version: 3.1054.0 + resolution: "@aws-sdk/client-dynamodb@npm:3.1054.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/credential-provider-node": "npm:^3.972.45" + "@aws-sdk/dynamodb-codec": "npm:^3.973.14" + "@aws-sdk/middleware-endpoint-discovery": "npm:^3.972.14" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/fetch-http-handler": "npm:^5.4.3" + "@smithy/node-http-handler": "npm:^4.7.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/d244a35f9c365ca33879c7d33575d4a810b90d58b720b7bf184e4edc600de1a1a39f3fb7f6a5ccb5231c84bc0de578c0691e8279f7f997091bc818842ee29240 + languageName: node + linkType: hard + "@aws-sdk/client-ec2@npm:^3.1009.0": version: 3.1014.0 resolution: "@aws-sdk/client-ec2@npm:3.1014.0" @@ -597,6 +618,22 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/core@npm:^3.974.14": + version: 3.974.14 + resolution: "@aws-sdk/core@npm:3.974.14" + dependencies: + "@aws-sdk/types": "npm:^3.973.9" + "@aws-sdk/xml-builder": "npm:^3.972.26" + "@aws/lambda-invoke-store": "npm:^0.2.2" + "@smithy/core": "npm:^3.24.3" + "@smithy/signature-v4": "npm:^5.4.2" + "@smithy/types": "npm:^4.14.2" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/5d3bfdf0a1849442b53e04b55974179e72e01e5474323c5cffbaea15522ad4ac008fd013e981583758267152055e34c0fc0c9ff7febefa7716e102b5812a0d75 + languageName: node + linkType: hard + "@aws-sdk/crc64-nvme@npm:^3.972.5": version: 3.972.5 resolution: "@aws-sdk/crc64-nvme@npm:3.972.5" @@ -620,6 +657,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-env@npm:^3.972.40": + version: 3.972.40 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.40" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/4882aaa01c60642c5d04956b2bff21398f3cbdba04fc6adf82a208d1540ba9c25fdabf0caa805e556d091dc8bffcf9beb6e01302bbf90f3723faca46a6e54245 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-http@npm:^3.972.23": version: 3.972.23 resolution: "@aws-sdk/credential-provider-http@npm:3.972.23" @@ -638,6 +688,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-http@npm:^3.972.42": + version: 3.972.42 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.42" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/fetch-http-handler": "npm:^5.4.3" + "@smithy/node-http-handler": "npm:^4.7.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/3955a4af3d39d7a38b433a7f39b873bb1bc1633ca701af4aed0c61756234d3085123e99c6335726c43ccad4858d5570ecef4923fe6cb1bd3bc522f6ed67b8b63 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-ini@npm:^3.972.23": version: 3.972.23 resolution: "@aws-sdk/credential-provider-ini@npm:3.972.23" @@ -660,6 +725,27 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-ini@npm:^3.972.44": + version: 3.972.44 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.44" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/credential-provider-env": "npm:^3.972.40" + "@aws-sdk/credential-provider-http": "npm:^3.972.42" + "@aws-sdk/credential-provider-login": "npm:^3.972.44" + "@aws-sdk/credential-provider-process": "npm:^3.972.40" + "@aws-sdk/credential-provider-sso": "npm:^3.972.44" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.44" + "@aws-sdk/nested-clients": "npm:^3.997.12" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/credential-provider-imds": "npm:^4.3.2" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/34f3bb770df3cb31bf9129ea9e2fc72a122cd131054481d67c2dd5e509967771fd2c7426f574ba9a1bd855b8446add0125776353c7d63bc6a88b1ea5e6eda9f0 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-login@npm:^3.972.23": version: 3.972.23 resolution: "@aws-sdk/credential-provider-login@npm:3.972.23" @@ -676,6 +762,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-login@npm:^3.972.44": + version: 3.972.44 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.44" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/nested-clients": "npm:^3.997.12" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/9d03c914eb5ae92565908785cf1859ee830880b0198e9242fb653c1c075a08b87ae9da5640068a14ed2ac4edc36853e4c0a118fb40c1f59d8a09818e17450852 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-node@npm:^3.972.24": version: 3.972.24 resolution: "@aws-sdk/credential-provider-node@npm:3.972.24" @@ -696,6 +796,25 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-node@npm:^3.972.45": + version: 3.972.45 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.45" + dependencies: + "@aws-sdk/credential-provider-env": "npm:^3.972.40" + "@aws-sdk/credential-provider-http": "npm:^3.972.42" + "@aws-sdk/credential-provider-ini": "npm:^3.972.44" + "@aws-sdk/credential-provider-process": "npm:^3.972.40" + "@aws-sdk/credential-provider-sso": "npm:^3.972.44" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.44" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/credential-provider-imds": "npm:^4.3.2" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/0060df16c887a68c2878087fecfa5f20a110bfce60d8227844a975ad04ae064d54a4b205f42eda88108a3f55c5e652cc6227d49be7742ca0ed6a9f19bb3c5ffd + languageName: node + linkType: hard + "@aws-sdk/credential-provider-process@npm:^3.972.21": version: 3.972.21 resolution: "@aws-sdk/credential-provider-process@npm:3.972.21" @@ -710,6 +829,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-process@npm:^3.972.40": + version: 3.972.40 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.40" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/e87bafcd71005b7947f5cfdd34fad36225f2922206d5039e93f3fdbc55d5b29d7083f766a02084621006cd219f206e72f3cb75a0806cf4e6ac0acdb924bb9fc7 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-sso@npm:^3.972.23": version: 3.972.23 resolution: "@aws-sdk/credential-provider-sso@npm:3.972.23" @@ -726,6 +858,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-sso@npm:^3.972.44": + version: 3.972.44 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.44" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/nested-clients": "npm:^3.997.12" + "@aws-sdk/token-providers": "npm:3.1054.0" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/cfcba3bfc222cb307d40b5de2f699462b9681b5ed17dfe1068d9ddf2795bdf43b9e9bc1a2aa6414d3e691ef4cd5b83abe1841a22586f3f4f1bcf760b7e85a1db + languageName: node + linkType: hard + "@aws-sdk/credential-provider-web-identity@npm:^3.972.23": version: 3.972.23 resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.23" @@ -741,6 +888,42 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-web-identity@npm:^3.972.44": + version: 3.972.44 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.44" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/nested-clients": "npm:^3.997.12" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/55a4c5a4e74e2ea71eccac48e45bf9432c67ae5b04b7e40042697f63c2f759c62a6e60adaca8c8763aa405be504d11e7e2537a93a7f9576ab6c62de42cf6e755 + languageName: node + linkType: hard + +"@aws-sdk/dynamodb-codec@npm:^3.973.14": + version: 3.973.14 + resolution: "@aws-sdk/dynamodb-codec@npm:3.973.14" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/a8ef09683b80cacf21a9f5229aaac797c6ab2c51689a1c4827ca03d990aff7f5e5097005699ebb4fe82f09da2a8d73d4e6791435312fd00e0e422f18a6baa61c + languageName: node + linkType: hard + +"@aws-sdk/endpoint-cache@npm:^3.972.5": + version: 3.972.5 + resolution: "@aws-sdk/endpoint-cache@npm:3.972.5" + dependencies: + mnemonist: "npm:0.38.3" + tslib: "npm:^2.6.2" + checksum: 10c0/10707330728ef1f9ca74134ed19a5d93c28d9af4cf785d53ced74a645ae3f6ccc7cb75ffda552dad49ad3ef1aaa27f829431851f530be2c914b61399dd5342b6 + languageName: node + linkType: hard + "@aws-sdk/lib-storage@npm:^3.1009.0": version: 3.1014.0 resolution: "@aws-sdk/lib-storage@npm:3.1014.0" @@ -773,6 +956,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-endpoint-discovery@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/middleware-endpoint-discovery@npm:3.972.14" + dependencies: + "@aws-sdk/endpoint-cache": "npm:^3.972.5" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/46cccece5014365b26f7734e82502c3a4120fa58b302a05e9c17078b672d858e2ffaaec20f8a3d6d23a029ae92c850834407d9e47a8bd1e3b3336bb6a6bae957 + languageName: node + linkType: hard + "@aws-sdk/middleware-expect-continue@npm:^3.972.8": version: 3.972.8 resolution: "@aws-sdk/middleware-expect-continue@npm:3.972.8" @@ -979,6 +1175,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/nested-clients@npm:^3.997.12": + version: 3.997.12 + resolution: "@aws-sdk/nested-clients@npm:3.997.12" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.29" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/fetch-http-handler": "npm:^5.4.3" + "@smithy/node-http-handler": "npm:^4.7.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/32d2648db9a04fc1e6f52f3c31af9668f1c7ace85476666e49c3836caef430a1ca85f74414894340eec8e328ba58b88b5fd260b5b56bcd862e34cc3b453f4b38 + languageName: node + linkType: hard + "@aws-sdk/region-config-resolver@npm:^3.972.9": version: 3.972.9 resolution: "@aws-sdk/region-config-resolver@npm:3.972.9" @@ -1006,6 +1220,18 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/signature-v4-multi-region@npm:^3.996.29": + version: 3.996.29 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.29" + dependencies: + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/signature-v4": "npm:^5.4.2" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/4a2cfa83075cbbcc78ba2bf25d6d5fd166c33b6c94663d57a1e611650a554c0c091d4a4ae957901b72ae2e87b3bfd1a96816872b81d005a8c3ca5e21779c6a7c + languageName: node + linkType: hard + "@aws-sdk/token-providers@npm:3.1014.0": version: 3.1014.0 resolution: "@aws-sdk/token-providers@npm:3.1014.0" @@ -1021,6 +1247,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/token-providers@npm:3.1054.0": + version: 3.1054.0 + resolution: "@aws-sdk/token-providers@npm:3.1054.0" + dependencies: + "@aws-sdk/core": "npm:^3.974.14" + "@aws-sdk/nested-clients": "npm:^3.997.12" + "@aws-sdk/types": "npm:^3.973.9" + "@smithy/core": "npm:^3.24.3" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/27d315a3238c50fc88ace785f4a0efe6e49034ab61af7af5585b43cef235fc740cd6ac7bd9fed789344ef1eb289e4f871caa332454f8bdcb923826db67132830 + languageName: node + linkType: hard + "@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.4.1": version: 3.914.0 resolution: "@aws-sdk/types@npm:3.914.0" @@ -1041,6 +1281,16 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/types@npm:^3.973.9": + version: 3.973.9 + resolution: "@aws-sdk/types@npm:3.973.9" + dependencies: + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/8164f908a004c23927109b86f44e9d112804aaa32cd654b260f79b7c2c2f9fb6802b72ccf73cbbf6fa1c613e660ce5b45835018db94713a59714e8ff762aaf2a + languageName: node + linkType: hard + "@aws-sdk/util-arn-parser@npm:^3.972.3": version: 3.972.3 resolution: "@aws-sdk/util-arn-parser@npm:3.972.3" @@ -1126,6 +1376,17 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/xml-builder@npm:^3.972.26": + version: 3.972.26 + resolution: "@aws-sdk/xml-builder@npm:3.972.26" + dependencies: + "@smithy/types": "npm:^4.14.2" + fast-xml-parser: "npm:5.7.3" + tslib: "npm:^2.6.2" + checksum: 10c0/f9501c0e475455d6ce6dd66aba781d07094a7e80c09a0b894e755a373afd6ec0f0f8f235e46d92f22902db7c3f9225beeab3b22993dd429999a7e618a8f9f9f1 + languageName: node + linkType: hard + "@aws/lambda-invoke-store@npm:0.2.3": version: 0.2.3 resolution: "@aws/lambda-invoke-store@npm:0.2.3" @@ -3292,6 +3553,13 @@ __metadata: languageName: node linkType: hard +"@nodable/entities@npm:^2.1.0": + version: 2.1.0 + resolution: "@nodable/entities@npm:2.1.0" + checksum: 10c0/5a4cba2b61a5b6c726328b18b1de6d033cae4a658a118644bf31e0bcbda126ea7b69385043dc556cf1ed859b9ca220e82b81b5e5c48ef1b519fb8ec104575dee + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4510,6 +4778,17 @@ __metadata: languageName: node linkType: hard +"@smithy/core@npm:^3.24.3, @smithy/core@npm:^3.24.4": + version: 3.24.4 + resolution: "@smithy/core@npm:3.24.4" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/0951177e0556baf63093e30e2768991153f364655aafcaf4fca297b1d120ff3a7f445f068c3de65e4224dbecd99d86ca9e1f5c7d96841756905454c321951cd5 + languageName: node + linkType: hard + "@smithy/credential-provider-imds@npm:^4.2.12": version: 4.2.12 resolution: "@smithy/credential-provider-imds@npm:4.2.12" @@ -4523,6 +4802,17 @@ __metadata: languageName: node linkType: hard +"@smithy/credential-provider-imds@npm:^4.3.2": + version: 4.3.4 + resolution: "@smithy/credential-provider-imds@npm:4.3.4" + dependencies: + "@smithy/core": "npm:^3.24.4" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/4f9dbe2ad0bf193dd5d2485a4b1b5cb2cfa698a54f32a90eac70d479ac8710cf349d141d31d7388d47136f7fb4f90da565a3b6226d2e30d6245b83c907b452ac + languageName: node + linkType: hard + "@smithy/eventstream-codec@npm:^4.2.12": version: 4.2.12 resolution: "@smithy/eventstream-codec@npm:4.2.12" @@ -4591,6 +4881,17 @@ __metadata: languageName: node linkType: hard +"@smithy/fetch-http-handler@npm:^5.4.3": + version: 5.4.4 + resolution: "@smithy/fetch-http-handler@npm:5.4.4" + dependencies: + "@smithy/core": "npm:^3.24.4" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/7818f546de5507226328b0647db0ef83974a588d82aed6ccd9d6741c3f256475ce3151034608c93c964c9febcb3ea91e814c3b20365c88dca26c02d8e3b4380e + languageName: node + linkType: hard + "@smithy/hash-blob-browser@npm:^4.2.13": version: 4.2.13 resolution: "@smithy/hash-blob-browser@npm:4.2.13" @@ -4756,6 +5057,17 @@ __metadata: languageName: node linkType: hard +"@smithy/node-http-handler@npm:^4.7.3": + version: 4.7.4 + resolution: "@smithy/node-http-handler@npm:4.7.4" + dependencies: + "@smithy/core": "npm:^3.24.4" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/12e4508aab964756ad54987884c399b4a957a795a5e8244e639bec699d42f5bf7fe286fa1a345d07bb532623dcc4220fd8eee043387b9242185c3cafcc235cb2 + languageName: node + linkType: hard + "@smithy/property-provider@npm:^4.2.12": version: 4.2.12 resolution: "@smithy/property-provider@npm:4.2.12" @@ -4841,6 +5153,17 @@ __metadata: languageName: node linkType: hard +"@smithy/signature-v4@npm:^5.4.2": + version: 5.4.4 + resolution: "@smithy/signature-v4@npm:5.4.4" + dependencies: + "@smithy/core": "npm:^3.24.4" + "@smithy/types": "npm:^4.14.2" + tslib: "npm:^2.6.2" + checksum: 10c0/08f5c12c21f4e0beb54b837d8399f9743d4313ec0baf37f4d963aa90623f68858f8c92015ab17a20b5196c448d1504a2ca9777084efd9c17b6fd7faad54d9921 + languageName: node + linkType: hard + "@smithy/smithy-client@npm:^4.12.7": version: 4.12.7 resolution: "@smithy/smithy-client@npm:4.12.7" @@ -4874,6 +5197,15 @@ __metadata: languageName: node linkType: hard +"@smithy/types@npm:^4.14.2": + version: 4.14.2 + resolution: "@smithy/types@npm:4.14.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/8bdad874d9e47233c6f2f74ac001e810b87b0a7c6504b9a9490536c9733ffec36b7b2a73010b31044f22553819a8d1e8bb9b2310f09257b1278a048a5f9679f8 + languageName: node + linkType: hard + "@smithy/types@npm:^4.8.0": version: 4.8.0 resolution: "@smithy/types@npm:4.8.0" @@ -7698,6 +8030,16 @@ __metadata: languageName: node linkType: hard +"fast-xml-builder@npm:^1.1.7": + version: 1.2.0 + resolution: "fast-xml-builder@npm:1.2.0" + dependencies: + path-expression-matcher: "npm:^1.5.0" + xml-naming: "npm:^0.1.0" + checksum: 10c0/84bb105cd04e91d6dcb746c4dbaeb12903b510e7ab9a06ffde55b5a582e005559a87d84467f18a655c6c4baf098f696fd74cee3cbe1aea9d01385907768ba32d + languageName: node + linkType: hard + "fast-xml-parser@npm:5.5.8": version: 5.5.8 resolution: "fast-xml-parser@npm:5.5.8" @@ -7711,6 +8053,20 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:5.7.3": + version: 5.7.3 + resolution: "fast-xml-parser@npm:5.7.3" + dependencies: + "@nodable/entities": "npm:^2.1.0" + fast-xml-builder: "npm:^1.1.7" + path-expression-matcher: "npm:^1.5.0" + strnum: "npm:^2.2.3" + bin: + fxparser: src/cli/cli.js + checksum: 10c0/eeb802855e852ce16121396297f04131c6dbc74f863be94f19e26e386656bdb31677af469ddc6627983a48b99d8842888460ac5413063cb648fde547bb579978 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" @@ -9173,6 +9529,15 @@ __metadata: languageName: node linkType: hard +"mnemonist@npm:0.38.3": + version: 0.38.3 + resolution: "mnemonist@npm:0.38.3" + dependencies: + obliterator: "npm:^1.6.1" + checksum: 10c0/064aa1ee1a89fce2754423b3617c598fd65bc34311eb3c01dc063976f6b819b073bd23532415cf8c92240157b4c8fbb7ec5d79d717f2bd4fcd95d8131cb23acb + languageName: node + linkType: hard + "moment-timezone@npm:^0.6.0": version: 0.6.0 resolution: "moment-timezone@npm:0.6.0" @@ -9575,6 +9940,13 @@ __metadata: languageName: node linkType: hard +"obliterator@npm:^1.6.1": + version: 1.6.1 + resolution: "obliterator@npm:1.6.1" + checksum: 10c0/5fad57319aae0ef6e34efa640541d41c2dd9790a7ab808f17dcb66c83a81333963fc2dfcfa6e1b62158e5cef6291cdcf15c503ad6c3de54b2227dd4c3d7e1b55 + languageName: node + linkType: hard + "obug@npm:^2.1.1": version: 2.1.1 resolution: "obug@npm:2.1.1" @@ -9815,6 +10187,13 @@ __metadata: languageName: node linkType: hard +"path-expression-matcher@npm:^1.5.0": + version: 1.5.0 + resolution: "path-expression-matcher@npm:1.5.0" + checksum: 10c0/646cb5bc66cd7d809a52288336f3ac1e6223f156fd8e912936e490e590f7f93e8056d4fd25fcbcc7da61bb698fa520112cb050372a3f65e7b79bd4afa0f77610 + languageName: node + linkType: hard + "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -10778,6 +11157,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.2.3": + version: 2.3.0 + resolution: "strnum@npm:2.3.0" + checksum: 10c0/8d29ea0789df22dfa6101153573c76ce12fb065ed0807eb99cc64624cd7f3d67a5aa0db507e75ab985ca23908cc4f02c65f3359ad762cb3659e3d6456e76e143 + languageName: node + linkType: hard + "supports-color@npm:^7, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -11476,6 +11862,13 @@ __metadata: languageName: node linkType: hard +"xml-naming@npm:^0.1.0": + version: 0.1.0 + resolution: "xml-naming@npm:0.1.0" + checksum: 10c0/8c7614865361bcb7e53e3e091dac21c567e2b92d447919b2f072775aa9dcfc94a5255bd52fbaa0fd53c93513e53a23a6a835218ad2af512451dbc678392f85fe + languageName: node + linkType: hard + "xml2js@npm:0.5.0": version: 0.5.0 resolution: "xml2js@npm:0.5.0" diff --git a/main.tf b/main.tf index a9a79c87a3..103b28ccb4 100644 --- a/main.tf +++ b/main.tf @@ -279,6 +279,9 @@ module "runners" { metrics = var.metrics job_retry = var.job_retry + + installation_token_table_name = aws_dynamodb_table.installation_tokens.name + installation_token_table_arn = aws_dynamodb_table.installation_tokens.arn } module "runner_binaries" { diff --git a/modules/multi-runner/runners.tf b/modules/multi-runner/runners.tf index 59b6307aa0..3e4e45fc53 100644 --- a/modules/multi-runner/runners.tf +++ b/modules/multi-runner/runners.tf @@ -122,5 +122,8 @@ module "runners" { job_retry = each.value.runner_config.job_retry + installation_token_table_name = aws_dynamodb_table.installation_tokens.name + installation_token_table_arn = aws_dynamodb_table.installation_tokens.arn + metrics = var.metrics } diff --git a/modules/multi-runner/token-cache.tf b/modules/multi-runner/token-cache.tf new file mode 100644 index 0000000000..39d25d7d39 --- /dev/null +++ b/modules/multi-runner/token-cache.tf @@ -0,0 +1,27 @@ +resource "aws_dynamodb_table" "installation_tokens" { + name = "${var.prefix}-installation-tokens" + billing_mode = "PAY_PER_REQUEST" + hash_key = "installation_id" + + attribute { + name = "installation_id" + type = "N" + } + + ttl { + attribute_name = "ttl" + enabled = true + } + + point_in_time_recovery { + enabled = false + } + + server_side_encryption { + enabled = true + } + + tags = merge(local.tags, { + Name = "${var.prefix}-installation-tokens" + }) +} diff --git a/modules/runners/pool.tf b/modules/runners/pool.tf index 53c5d1c2cd..e7735d7062 100644 --- a/modules/runners/pool.tf +++ b/modules/runners/pool.tf @@ -61,6 +61,8 @@ module "pool" { tags = local.tags lambda_tags = var.lambda_tags arn_ssm_parameters_path_config = local.arn_ssm_parameters_path_config + installation_token_table_name = var.installation_token_table_name + installation_token_table_arn = var.installation_token_table_arn } aws_partition = var.aws_partition diff --git a/modules/runners/pool/main.tf b/modules/runners/pool/main.tf index 5363f3c3fb..ce4d332db7 100644 --- a/modules/runners/pool/main.tf +++ b/modules/runners/pool/main.tf @@ -49,6 +49,7 @@ resource "aws_lambda_function" "pool" { ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS = jsonencode(var.config.runner.enable_on_demand_failover_for_errors) SSM_PARAMETER_STORE_TAGS = var.config.lambda.parameter_store_tags SCALE_ERRORS = jsonencode(var.config.runner.scale_errors) + INSTALLATION_TOKEN_TABLE_NAME = var.config.installation_token_table_name } } @@ -106,6 +107,24 @@ resource "aws_iam_role_policy" "pool_logging" { }) } +resource "aws_iam_role_policy" "pool_token_cache" { + name = "token-cache-policy" + role = aws_iam_role.pool.name + policy = data.aws_iam_policy_document.pool_token_cache.json +} + +data "aws_iam_policy_document" "pool_token_cache" { + statement { + effect = "Allow" + actions = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + ] + resources = [var.config.installation_token_table_arn] + } +} + resource "aws_iam_role_policy_attachment" "pool_vpc_execution_role" { count = length(var.config.lambda.subnet_ids) > 0 ? 1 : 0 role = aws_iam_role.pool.name diff --git a/modules/runners/scale-up.tf b/modules/runners/scale-up.tf index c5503f6394..9d21034bf9 100644 --- a/modules/runners/scale-up.tf +++ b/modules/runners/scale-up.tf @@ -62,6 +62,7 @@ resource "aws_lambda_function" "scale_up" { ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS = jsonencode(var.enable_on_demand_failover_for_errors) SCALE_ERRORS = jsonencode(var.scale_errors) JOB_RETRY_CONFIG = jsonencode(local.job_retry_config) + INSTALLATION_TOKEN_TABLE_NAME = var.installation_token_table_name } } @@ -137,6 +138,24 @@ resource "aws_iam_role_policy" "scale_up_logging" { }) } +resource "aws_iam_role_policy" "scale_up_token_cache" { + name = "token-cache-policy" + role = aws_iam_role.scale_up.name + policy = data.aws_iam_policy_document.scale_up_token_cache.json +} + +data "aws_iam_policy_document" "scale_up_token_cache" { + statement { + effect = "Allow" + actions = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + ] + resources = [var.installation_token_table_arn] + } +} + resource "aws_iam_role_policy" "service_linked_role" { count = var.create_service_linked_role_spot ? 1 : 0 name = "service_linked_role" diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index e2a33280b9..5f7f1f75dc 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -812,3 +812,13 @@ variable "parameter_store_tags" { type = map(string) default = {} } + +variable "installation_token_table_name" { + description = "Name of the DynamoDB table used to cache GitHub App installation tokens across Lambda invocations." + type = string +} + +variable "installation_token_table_arn" { + description = "ARN of the DynamoDB table used to cache GitHub App installation tokens." + type = string +} diff --git a/token-cache.tf b/token-cache.tf new file mode 100644 index 0000000000..39d25d7d39 --- /dev/null +++ b/token-cache.tf @@ -0,0 +1,27 @@ +resource "aws_dynamodb_table" "installation_tokens" { + name = "${var.prefix}-installation-tokens" + billing_mode = "PAY_PER_REQUEST" + hash_key = "installation_id" + + attribute { + name = "installation_id" + type = "N" + } + + ttl { + attribute_name = "ttl" + enabled = true + } + + point_in_time_recovery { + enabled = false + } + + server_side_encryption { + enabled = true + } + + tags = merge(local.tags, { + Name = "${var.prefix}-installation-tokens" + }) +}