diff --git a/README.md b/README.md index bfa4fb19f7..cd756feec9 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ Talk to the forestkeepers in the `runners-channel` on Slack. | [runners\_ssm\_housekeeper](#input\_runners\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.

`schedule_expression`: is used to configure the schedule for the lambda.
`enabled`: enable or disable the lambda trigger via the EventBridge.
`lambda_memory_size`: lambda memery size limit.
`lambda_timeout`: timeout for the lambda in seconds.
`config`: configuration for the lambda function. Token path will be read by default from the module. |
object({
schedule_expression = optional(string, "rate(1 day)")
enabled = optional(bool, true)
lambda_memory_size = optional(number, 512)
lambda_timeout = optional(number, 60)
config = object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
})
})
|
{
"config": {}
}
| no | | [scale\_down\_schedule\_expression](#input\_scale\_down\_schedule\_expression) | Scheduler expression to check every x for scale down. | `string` | `"cron(*/5 * * * ? *)"` | no | | [scale\_up\_reserved\_concurrent\_executions](#input\_scale\_up\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no | -| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
use_prefix = optional(bool, true)
})
| `{}` | no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
webhook = optional(string, "webhook")
use_prefix = optional(bool, true)
})
| `{}` | no | | [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 | | [subnet\_ids](#input\_subnet\_ids) | List of subnets in which the action runner instances will be launched. The subnets need to exist in the configured VPC (`vpc_id`), and must reside in different availability zones (see https://github.com/philips-labs/terraform-aws-github-runner/issues/2904) | `list(string)` | n/a | yes | | [syncer\_lambda\_s3\_key](#input\_syncer\_lambda\_s3\_key) | S3 key for syncer lambda function. Required if using an S3 bucket to specify lambdas. | `string` | `null` | no | diff --git a/docs/configuration.md b/docs/configuration.md index 44ac5ef394..29980fc68f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -22,7 +22,7 @@ The module uses the AWS System Manager Parameter Store to store configuration fo | `ssm_paths.root/var.prefix?/app/` | App secrets used by Lambda's | | `ssm_paths.root/var.prefix?/runners/config/` | Configuration parameters used by runner start script | | `ssm_paths.root/var.prefix?/runners/tokens/` | Either JIT configuration (ephemeral runners) or registration tokens (non ephemeral runners) generated by the control plane (scale-up lambda), and consumed by the start script on the runner to activate / register the runner. | - +| `ssm_paths.root/var.prefix?/webhook/runner-matcher-config` | Runner matcher config used by webhook to decide the target for the webhook event. | Available configuration parameters: | Parameter name | Description | diff --git a/lambdas/functions/control-plane/src/lambda.test.ts b/lambdas/functions/control-plane/src/lambda.test.ts index 9e2f5e0ce9..f4d28ccd79 100644 --- a/lambdas/functions/control-plane/src/lambda.test.ts +++ b/lambdas/functions/control-plane/src/lambda.test.ts @@ -64,6 +64,7 @@ jest.mock('./scale-runners/scale-down'); jest.mock('./pool/pool'); jest.mock('./scale-runners/ssm-housekeeper'); jest.mock('@terraform-aws-github-runner/aws-powertools-util'); +jest.mock('@terraform-aws-github-runner/aws-ssm-util'); // Docs for testing async with jest: https://jestjs.io/docs/tutorial-async describe('Test scale up lambda wrapper.', () => { diff --git a/lambdas/functions/webhook/src/ConfigResolver.ts b/lambdas/functions/webhook/src/ConfigResolver.ts index 2d72943e0c..15a4ed6589 100644 --- a/lambdas/functions/webhook/src/ConfigResolver.ts +++ b/lambdas/functions/webhook/src/ConfigResolver.ts @@ -1,15 +1,39 @@ -import { QueueConfig } from './sqs'; +import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; +import { RunnerMatcherConfig } from './sqs'; +import { logger } from '@terraform-aws-github-runner/aws-powertools-util'; export class Config { - public repositoryAllowList: Array; - public queuesConfig: Array; - public workflowJobEventSecondaryQueue; - - constructor() { - const repositoryAllowListEnv = process.env.REPOSITORY_ALLOW_LIST || '[]'; - this.repositoryAllowList = JSON.parse(repositoryAllowListEnv) as Array; - const queuesConfigEnv = process.env.RUNNER_CONFIG || '[]'; - this.queuesConfig = JSON.parse(queuesConfigEnv) as Array; - this.workflowJobEventSecondaryQueue = process.env.SQS_WORKFLOW_JOB_QUEUE || undefined; + repositoryAllowList: Array; + static matcherConfig: Array | undefined; + static webhookSecret: string | undefined; + workflowJobEventSecondaryQueue: string | undefined; + + constructor(repositoryAllowList: Array, workflowJobEventSecondaryQueue: string | undefined) { + this.repositoryAllowList = repositoryAllowList; + + this.workflowJobEventSecondaryQueue = workflowJobEventSecondaryQueue; + } + + static async load(): Promise { + const repositoryAllowListEnv = process.env.REPOSITORY_ALLOW_LIST ?? '[]'; + const repositoryAllowList = JSON.parse(repositoryAllowListEnv) as Array; + // load parallel config if not cached + if (!Config.matcherConfig) { + const matcherConfigPath = + process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH ?? '/github-runner/runner-matcher-config'; + const [matcherConfigVal, webhookSecret] = await Promise.all([ + getParameter(matcherConfigPath), + getParameter(process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET), + ]); + Config.webhookSecret = webhookSecret; + Config.matcherConfig = JSON.parse(matcherConfigVal) as Array; + logger.debug('Loaded queues config', { matcherConfig: Config.matcherConfig }); + } + const workflowJobEventSecondaryQueue = process.env.SQS_WORKFLOW_JOB_QUEUE ?? undefined; + return new Config(repositoryAllowList, workflowJobEventSecondaryQueue); + } + + static reset(): void { + Config.matcherConfig = undefined; } } diff --git a/lambdas/functions/webhook/src/lambda.test.ts b/lambdas/functions/webhook/src/lambda.test.ts index 4b521140a4..6d1ed5da0f 100644 --- a/lambdas/functions/webhook/src/lambda.test.ts +++ b/lambdas/functions/webhook/src/lambda.test.ts @@ -5,6 +5,7 @@ import { mocked } from 'jest-mock'; import { githubWebhook } from './lambda'; import { handle } from './webhook'; import ValidationError from './ValidatonError'; +import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; const event: APIGatewayEvent = { body: JSON.stringify(''), @@ -73,8 +74,13 @@ const context: Context = { }; jest.mock('./webhook'); +jest.mock('@terraform-aws-github-runner/aws-ssm-util'); describe('Test scale up lambda wrapper.', () => { + beforeEach(() => { + const mockedGet = mocked(getParameter); + mockedGet.mockResolvedValue('[]'); + }); it('Happy flow, resolve.', async () => { const mock = mocked(handle); mock.mockImplementation(() => { diff --git a/lambdas/functions/webhook/src/lambda.ts b/lambdas/functions/webhook/src/lambda.ts index 089d620599..4bc2e3c366 100644 --- a/lambdas/functions/webhook/src/lambda.ts +++ b/lambdas/functions/webhook/src/lambda.ts @@ -16,7 +16,7 @@ middy(githubWebhook).use(captureLambdaHandler(tracer)); export async function githubWebhook(event: APIGatewayEvent, context: Context): Promise { setContext(context, 'lambda.ts'); - const config = new Config(); + const config = await Config.load(); logger.logEventIfEnabled(event); logger.debug('Loading config', { config }); diff --git a/lambdas/functions/webhook/src/local.ts b/lambdas/functions/webhook/src/local.ts index b73062c967..ddedb552f4 100644 --- a/lambdas/functions/webhook/src/local.ts +++ b/lambdas/functions/webhook/src/local.ts @@ -5,12 +5,12 @@ import { handle } from './webhook'; import { Config } from './ConfigResolver'; const app = express(); -const config = new Config(); +const config = Config.load(); app.use(bodyParser.json()); -app.post('/event_handler', (req, res) => { - handle(req.headers, JSON.stringify(req.body), config) +app.post('/event_handler', async (req, res) => { + handle(req.headers, JSON.stringify(req.body), await config) .then((c) => res.status(c.statusCode).end()) .catch((e) => { console.log(e); diff --git a/lambdas/functions/webhook/src/sqs/index.test.ts b/lambdas/functions/webhook/src/sqs/index.test.ts index bda003a8da..c8b66adcb5 100644 --- a/lambdas/functions/webhook/src/sqs/index.test.ts +++ b/lambdas/functions/webhook/src/sqs/index.test.ts @@ -3,6 +3,8 @@ import { SendMessageCommandInput } from '@aws-sdk/client-sqs'; import { ActionRequestMessage, GithubWorkflowEvent, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '.'; import workflowjob_event from '../../test/resources/github_workflowjob_event.json'; import { Config } from '../ConfigResolver'; +import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; +import { mocked } from 'jest-mock'; const mockSQS = { sendMessage: jest.fn(() => { @@ -12,6 +14,7 @@ const mockSQS = { jest.mock('@aws-sdk/client-sqs', () => ({ SQS: jest.fn().mockImplementation(() => mockSQS), })); +jest.mock('@terraform-aws-github-runner/aws-ssm-util'); describe('Test sending message to SQS.', () => { const queueUrl = 'https://sqs.eu-west-1.amazonaws.com/123456789/queued-builds'; @@ -74,6 +77,10 @@ describe('Test sending message to SQS.', () => { QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/123456789/webhook_events_workflow_job_queue', MessageBody: JSON.stringify(message), }; + beforeEach(() => { + const mockedGet = mocked(getParameter); + mockedGet.mockResolvedValue('[]'); + }); afterEach(() => { jest.clearAllMocks(); }); @@ -81,7 +88,7 @@ describe('Test sending message to SQS.', () => { it('sends webhook events to workflow job queue', async () => { // Arrange process.env.SQS_WORKFLOW_JOB_QUEUE = sqsMessage.QueueUrl; - const config = new Config(); + const config = await Config.load(); // Act const result = await sendWebhookEventToWorkflowJobQueue(message, config); @@ -94,7 +101,7 @@ describe('Test sending message to SQS.', () => { it('Does not send webhook events to workflow job event copy queue', async () => { // Arrange process.env.SQS_WORKFLOW_JOB_QUEUE = ''; - const config = new Config(); + const config = await Config.load(); // Act await sendWebhookEventToWorkflowJobQueue(message, config); @@ -105,7 +112,7 @@ describe('Test sending message to SQS.', () => { it('Catch the exception when even copy queue throws exception', async () => { // Arrange process.env.SQS_WORKFLOW_JOB_QUEUE = sqsMessage.QueueUrl; - const config = new Config(); + const config = await Config.load(); const mockSQS = { sendMessage: jest.fn(() => { diff --git a/lambdas/functions/webhook/src/sqs/index.ts b/lambdas/functions/webhook/src/sqs/index.ts index 7f506b374a..8a1e8e20af 100644 --- a/lambdas/functions/webhook/src/sqs/index.ts +++ b/lambdas/functions/webhook/src/sqs/index.ts @@ -20,9 +20,9 @@ export interface MatcherConfig { exactMatch: boolean; } -export type RunnerConfig = QueueConfig[]; +export type RunnerConfig = RunnerMatcherConfig[]; -export interface QueueConfig { +export interface RunnerMatcherConfig { matcherConfig: MatcherConfig; id: string; arn: string; diff --git a/lambdas/functions/webhook/src/webhook/index.test.ts b/lambdas/functions/webhook/src/webhook/index.test.ts index d4e73ecdc0..cacbde23f5 100644 --- a/lambdas/functions/webhook/src/webhook/index.test.ts +++ b/lambdas/functions/webhook/src/webhook/index.test.ts @@ -37,18 +37,17 @@ describe('handler', () => { let originalError: Console['error']; let config: Config; - beforeEach(() => { + beforeEach(async () => { process.env = { ...cleanEnv }; nock.disableNetConnect(); - config = new Config(); originalError = console.error; console.error = jest.fn(); jest.clearAllMocks(); jest.resetAllMocks(); - const mockedGet = mocked(getParameter); - mockedGet.mockResolvedValueOnce(GITHUB_APP_WEBHOOK_SECRET); + mockSSMResponse(); + config = await Config.load(); }); afterEach(() => { @@ -73,8 +72,8 @@ describe('handler', () => { }); describe('Test for workflowjob event: ', () => { - beforeEach(() => { - config = createConfig(undefined, runnerConfig); + beforeEach(async () => { + config = await createConfig(undefined, runnerConfig); }); it('handles workflow job events with 256 hash signature', async () => { @@ -122,7 +121,7 @@ describe('handler', () => { it('does not handle workflow_job events from unlisted repositories', async () => { const event = JSON.stringify(workflowjob_event); - config = createConfig(['NotCodertocat/Hello-World']); + config = await createConfig(['NotCodertocat/Hello-World']); await expect( handle({ 'X-Hub-Signature-256': await webhooks.sign(event), 'X-GitHub-Event': 'workflow_job' }, event, config), ).rejects.toMatchObject({ @@ -133,7 +132,7 @@ describe('handler', () => { it('handles workflow_job events without installation id', async () => { const event = JSON.stringify({ ...workflowjob_event, installation: null }); - config = createConfig(['philips-labs/terraform-aws-github-runner']); + config = await createConfig(['philips-labs/terraform-aws-github-runner']); const resp = await handle( { 'X-Hub-Signature-256': await webhooks.sign(event), 'X-GitHub-Event': 'workflow_job' }, event, @@ -145,7 +144,7 @@ describe('handler', () => { it('handles workflow_job events from allow listed repositories', async () => { const event = JSON.stringify(workflowjob_event); - config = createConfig(['philips-labs/terraform-aws-github-runner']); + config = await createConfig(['philips-labs/terraform-aws-github-runner']); const resp = await handle( { 'X-Hub-Signature-256': await webhooks.sign(event), 'X-GitHub-Event': 'workflow_job' }, event, @@ -156,7 +155,7 @@ describe('handler', () => { }); it('Check runner labels accept test job', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -189,7 +188,7 @@ describe('handler', () => { }); it('Check runner labels accept job with mixed order.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -222,7 +221,7 @@ describe('handler', () => { }); it('Check webhook accept jobs where not all labels are provided in job.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -255,7 +254,7 @@ describe('handler', () => { }); it('Check webhook does not accept jobs where not all labels are supported (single matcher).', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -282,7 +281,7 @@ describe('handler', () => { }); it('Check webhook does not accept jobs where the job labels are spread across label matchers.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -312,7 +311,7 @@ describe('handler', () => { }); it('Check webhook does not accept jobs where not all labels are supported by the runner.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -346,7 +345,7 @@ describe('handler', () => { }); it('Check webhook will accept jobs with a single acceptable label.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -380,7 +379,7 @@ describe('handler', () => { }); it('Check webhook will not accept jobs without correct label when job label check all is false.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -412,7 +411,7 @@ describe('handler', () => { expect(sendActionRequest).not.toBeCalled(); }); it('Check webhook will accept jobs for specific labels if workflow labels are specific', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -454,7 +453,7 @@ describe('handler', () => { }); }); it('Check webhook will accept jobs for latest labels if workflow labels are not specific', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -498,7 +497,7 @@ describe('handler', () => { }); it('Check webhook will accept jobs when matchers accepts multiple labels.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -599,12 +598,21 @@ describe('canRunJob', () => { }); }); -function createConfig(repositoryAllowList?: string[], runnerConfig?: RunnerConfig): Config { +async function createConfig(repositoryAllowList?: string[], runnerConfig?: RunnerConfig): Promise { if (repositoryAllowList) { process.env.REPOSITORY_ALLOW_LIST = JSON.stringify(repositoryAllowList); } - if (runnerConfig) { - process.env.RUNNER_CONFIG = JSON.stringify(runnerConfig); - } - return new Config(); + Config.reset(); + mockSSMResponse(runnerConfig); + return await Config.load(); +} +function mockSSMResponse(runnerConfigInput?: RunnerConfig) { + const mockedGet = mocked(getParameter); + mockedGet.mockImplementation((parameter_name) => { + const value = + parameter_name == '/github-runner/runner-matcher-config' + ? JSON.stringify(runnerConfigInput ?? runnerConfig) + : GITHUB_APP_WEBHOOK_SECRET; + return Promise.resolve(value); + }); } diff --git a/lambdas/functions/webhook/src/webhook/index.ts b/lambdas/functions/webhook/src/webhook/index.ts index 522f2dbd2e..92daf8fea2 100644 --- a/lambdas/functions/webhook/src/webhook/index.ts +++ b/lambdas/functions/webhook/src/webhook/index.ts @@ -1,11 +1,10 @@ import { Webhooks } from '@octokit/webhooks'; import { CheckRunEvent, WorkflowJobEvent } from '@octokit/webhooks-types'; import { createChildLogger } from '@terraform-aws-github-runner/aws-powertools-util'; -import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; import { IncomingHttpHeaders } from 'http'; import { Response } from '../lambda'; -import { QueueConfig, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '../sqs'; +import { RunnerMatcherConfig, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '../sqs'; import ValidationError from '../ValidatonError'; import { Config } from '../ConfigResolver'; @@ -21,7 +20,7 @@ export async function handle(headers: IncomingHttpHeaders, body: string, config: validateRepoInAllowList(event, config); - const response = await handleWorkflowJob(event, eventType, config.queuesConfig); + const response = await handleWorkflowJob(event, eventType, Config.matcherConfig!); await sendWebhookEventToWorkflowJobQueue({ workflowJobEvent: event }, config); return response; } @@ -36,15 +35,15 @@ function validateRepoInAllowList(event: WorkflowJobEvent, config: Config) { async function handleWorkflowJob( body: WorkflowJobEvent, githubEvent: string, - queuesConfig: Array, + matcherConfig: Array, ): Promise { const installationId = getInstallationId(body); if (body.action === 'queued') { // sort the queuesConfig by order of matcher config exact match, with all true matches lined up ahead. - queuesConfig.sort((a, b) => { + matcherConfig.sort((a, b) => { return a.matcherConfig.exactMatch === b.matcherConfig.exactMatch ? 0 : a.matcherConfig.exactMatch ? -1 : 1; }); - for (const queue of queuesConfig) { + for (const queue of matcherConfig) { if (canRunJob(body.workflow_job.labels, queue.matcherConfig.labelMatchers, queue.matcherConfig.exactMatch)) { await sendActionRequest({ id: body.workflow_job.id, @@ -96,7 +95,7 @@ export function canRunJob( async function verifySignature(headers: IncomingHttpHeaders, body: string): Promise { const signature = headers['x-hub-signature-256'] as string; const webhooks = new Webhooks({ - secret: await getParameter(process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET), + secret: Config.webhookSecret!, }); if ( diff --git a/main.tf b/main.tf index c7d3008d79..19a89752de 100644 --- a/main.tf +++ b/main.tf @@ -123,12 +123,15 @@ module "ssm" { module "webhook" { source = "./modules/webhook" - + ssm_paths = { + root = "${local.ssm_root_path}" + webhook = "${var.ssm_paths.webhook}" + } prefix = var.prefix tags = local.tags kms_key_arn = var.kms_key_arn - runner_config = { + runner_matcher_config = { (aws_sqs_queue.queued_builds.id) = { id : aws_sqs_queue.queued_builds.id arn : aws_sqs_queue.queued_builds.arn diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf index ba53e358d4..0ca41b9dd4 100644 --- a/modules/multi-runner/variables.tf +++ b/modules/multi-runner/variables.tf @@ -566,6 +566,7 @@ variable "ssm_paths" { root = optional(string, "github-action-runners") app = optional(string, "app") runners = optional(string, "runners") + webhook = optional(string, "webhook") }) default = {} } diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf index 0e1d0782a4..96bcbdbc3e 100644 --- a/modules/multi-runner/webhook.tf +++ b/modules/multi-runner/webhook.tf @@ -4,7 +4,11 @@ module "webhook" { tags = local.tags kms_key_arn = var.kms_key_arn - runner_config = local.runner_config + runner_matcher_config = local.runner_config + ssm_paths = { + root = local.ssm_root_path + webhook = var.ssm_paths.webhook + } sqs_workflow_job_queue = length(aws_sqs_queue.webhook_events_workflow_job_queue) > 0 ? aws_sqs_queue.webhook_events_workflow_job_queue[0] : null github_app_parameters = { diff --git a/modules/runners/README.md b/modules/runners/README.md index fd4ff65fd2..061c0adf20 100644 --- a/modules/runners/README.md +++ b/modules/runners/README.md @@ -208,9 +208,8 @@ yarn run dist | [s3\_runner\_binaries](#input\_s3\_runner\_binaries) | Bucket details for cached GitHub binary. |
object({
arn = string
id = string
key = string
})
| n/a | yes | | [scale\_down\_schedule\_expression](#input\_scale\_down\_schedule\_expression) | Scheduler expression to check every x for scale down. | `string` | `"cron(*/5 * * * ? *)"` | no | | [scale\_up\_reserved\_concurrent\_executions](#input\_scale\_up\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no | -| [sqs\_build\_queue](#input\_sqs\_build\_queue) | SQS queue to consume accepted build events. |
object({
arn = string
})
| n/a | yes | -| [ssm\_housekeeper](#input\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.

`schedule_expression`: is used to configure the schedule for the lambda.
`state`: state of the cloudwatch event rule. Valid values are `DISABLED`, `ENABLED`, and `ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS`.
`lambda_memory_size`: lambda memery size limit.
`lambda_timeout`: timeout for the lambda in seconds.
`config`: configuration for the lambda function. Token path will be read by default from the module. |
object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
lambda_memory_size = optional(number, 512)
lambda_timeout = optional(number, 60)
config = object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
})
})
|
{
"config": {}
}
| no | -| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secreets. |
object({
root = string
tokens = string
config = string
})
| n/a | yes | +| [ssm\_housekeeper](#input\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.

`schedule_expression`: is used to configure the schedule for the lambda.
`state`: state of the cloudwatch event rule. Valid values are `DISABLED`, `ENABLED`, and `ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS`.
`lambda_timeout`: timeout for the lambda in seconds.
`config`: configuration for the lambda function. Token path will be read by default from the module. |
object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
lambda_timeout = optional(number, 60)
config = object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
})
})
|
{
"config": {}
}
| no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = string
tokens = string
config = string
})
| n/a | yes | | [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 | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name. | `map(string)` | `{}` | no | | [tracing\_config](#input\_tracing\_config) | Configuration for lambda tracing. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index 6df5e08fd8..01f5211a2d 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -574,7 +574,7 @@ variable "enable_user_data_debug_logging" { } variable "ssm_paths" { - description = "The root path used in SSM to store configuration and secreets." + description = "The root path used in SSM to store configuration and secrets." type = object({ root = string tokens = string diff --git a/modules/webhook/README.md b/modules/webhook/README.md index 8249fb110d..38d9ac43bc 100644 --- a/modules/webhook/README.md +++ b/modules/webhook/README.md @@ -38,12 +38,14 @@ yarn run dist |------|---------| | [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | ~> 5.27 | +| [null](#requirement\_null) | ~> 3.2 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | ~> 5.27 | +| [null](#provider\_null) | ~> 3.2 | ## Modules @@ -67,6 +69,8 @@ No modules. | [aws_iam_role_policy_attachment.webhook_vpc_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_lambda_function.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | | [aws_lambda_permission.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_ssm_parameter.runner_matcher_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [null_resource.github_app_parameters](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | | [aws_iam_policy_document.lambda_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -92,8 +96,9 @@ No modules. | [repository\_white\_list](#input\_repository\_white\_list) | List of github repository full names (owner/repo\_name) that will be allowed to use the github app. Leave empty for no filtering. | `list(string)` | `[]` | no | | [role\_path](#input\_role\_path) | The path that will be added to the role; if not set, the environment name will be used. | `string` | `null` | no | | [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no | -| [runner\_config](#input\_runner\_config) | SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accecpts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied. |
map(object({
arn = string
id = string
fifo = bool
matcherConfig = object({
labelMatchers = list(list(string))
exactMatch = bool
priority = optional(number, 999)
})
}))
| n/a | yes | +| [runner\_matcher\_config](#input\_runner\_matcher\_config) | SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accepts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied. |
map(object({
arn = string
id = string
fifo = bool
matcherConfig = object({
labelMatchers = list(list(string))
exactMatch = bool
priority = optional(number, 999)
})
}))
| n/a | yes | | [sqs\_workflow\_job\_queue](#input\_sqs\_workflow\_job\_queue) | SQS queue to monitor github events. |
object({
id = string
arn = string
})
| `null` | no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = string
webhook = string
})
| n/a | yes | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name and environment. | `map(string)` | `{}` | no | | [tracing\_config](#input\_tracing\_config) | Configuration for lambda tracing. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | | [webhook\_lambda\_apigateway\_access\_log\_settings](#input\_webhook\_lambda\_apigateway\_access\_log\_settings) | Access log settings for webhook API gateway. |
object({
destination_arn = string
format = string
})
| `null` | no | diff --git a/modules/webhook/main.tf b/modules/webhook/main.tf index c49957efab..503332b563 100644 --- a/modules/webhook/main.tf +++ b/modules/webhook/main.tf @@ -55,3 +55,10 @@ resource "aws_apigatewayv2_integration" "webhook" { integration_method = "POST" integration_uri = aws_lambda_function.webhook.invoke_arn } + + +resource "aws_ssm_parameter" "runner_matcher_config" { + name = "${var.ssm_paths.root}/${var.ssm_paths.webhook}/runner-matcher-config" + type = "String" + value = jsonencode(local.runner_matcher_config_sorted) +} diff --git a/modules/webhook/policies/lambda-ssm.json b/modules/webhook/policies/lambda-ssm.json index efef0b907d..23864db305 100644 --- a/modules/webhook/policies/lambda-ssm.json +++ b/modules/webhook/policies/lambda-ssm.json @@ -3,11 +3,10 @@ "Statement": [ { "Effect": "Allow", - "Action": [ - "ssm:GetParameter" - ], + "Action": ["ssm:GetParameter"], "Resource": [ - "${github_app_webhook_secret_arn}" + "${github_app_webhook_secret_arn}", + "${parameter_runner_matcher_config_arn}" ] } ] diff --git a/modules/webhook/variables.tf b/modules/webhook/variables.tf index 38960b1683..c4ed409585 100644 --- a/modules/webhook/variables.tf +++ b/modules/webhook/variables.tf @@ -22,8 +22,8 @@ variable "tags" { default = {} } -variable "runner_config" { - description = "SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accecpts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied." +variable "runner_matcher_config" { + description = "SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accepts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied." type = map(object({ arn = string id = string @@ -35,7 +35,7 @@ variable "runner_config" { }) })) validation { - condition = try(var.runner_config.matcherConfig.priority, 999) >= 0 && try(var.runner_config.matcherConfig.priority, 999) < 1000 + condition = try(var.runner_matcher_config.matcherConfig.priority, 999) >= 0 && try(var.runner_matcher_config.matcherConfig.priority, 999) < 1000 error_message = "The priority of the matcher must be between 0 and 999." } } @@ -186,3 +186,11 @@ variable "tracing_config" { }) default = {} } + +variable "ssm_paths" { + description = "The root path used in SSM to store configuration and secrets." + type = object({ + root = string + webhook = string + }) +} diff --git a/modules/webhook/versions.tf b/modules/webhook/versions.tf index 1df1926c45..d780c7775c 100644 --- a/modules/webhook/versions.tf +++ b/modules/webhook/versions.tf @@ -6,5 +6,10 @@ terraform { source = "hashicorp/aws" version = "~> 5.27" } + + null = { + source = "hashicorp/null" + version = "~> 3.2" + } } } diff --git a/modules/webhook/webhook.tf b/modules/webhook/webhook.tf index e6388f8767..6b90839054 100644 --- a/modules/webhook/webhook.tf +++ b/modules/webhook/webhook.tf @@ -1,9 +1,9 @@ locals { # config with combined key and order - runner_config = { for k, v in var.runner_config : format("%03d-%s", v.matcherConfig.priority, k) => merge(v, { key = k }) } + runner_matcher_config = { for k, v in var.runner_matcher_config : format("%03d-%s", v.matcherConfig.priority, k) => merge(v, { key = k }) } # sorted list - runner_config_sorted = [for k in sort(keys(local.runner_config)) : local.runner_config[k]] + runner_matcher_config_sorted = [for k in sort(keys(local.runner_matcher_config)) : local.runner_matcher_config[k]] } @@ -30,8 +30,8 @@ resource "aws_lambda_function" "webhook" { POWERTOOLS_TRACER_CAPTURE_ERROR = var.tracing_config.capture_error PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.github_app_parameters.webhook_secret.name REPOSITORY_WHITE_LIST = jsonencode(var.repository_white_list) - RUNNER_CONFIG = jsonencode(local.runner_config_sorted) SQS_WORKFLOW_JOB_QUEUE = try(var.sqs_workflow_job_queue, null) != null ? var.sqs_workflow_job_queue.id : "" + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = aws_ssm_parameter.runner_matcher_config.name } } @@ -51,6 +51,9 @@ resource "aws_lambda_function" "webhook" { mode = var.tracing_config.mode } } + lifecycle { + replace_triggered_by = [aws_ssm_parameter.runner_matcher_config, null_resource.github_app_parameters] + } } resource "aws_cloudwatch_log_group" "webhook" { @@ -66,6 +69,15 @@ resource "aws_lambda_permission" "webhook" { function_name = aws_lambda_function.webhook.function_name principal = "apigateway.amazonaws.com" source_arn = "${aws_apigatewayv2_api.webhook.execution_arn}/*/*/${local.webhook_endpoint}" + lifecycle { + replace_triggered_by = [aws_ssm_parameter.runner_matcher_config, null_resource.github_app_parameters] + } +} + +resource "null_resource" "github_app_parameters" { + triggers = { + github_app_webhook_secret = var.github_app_parameters.webhook_secret.name + } } data "aws_iam_policy_document" "lambda_assume_role_policy" { @@ -106,7 +118,7 @@ resource "aws_iam_role_policy" "webhook_sqs" { role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/policies/lambda-publish-sqs-policy.json", { - sqs_resource_arns = jsonencode([for k, v in var.runner_config : v.arn]) + sqs_resource_arns = jsonencode([for k, v in var.runner_matcher_config : v.arn]) kms_key_arn = var.kms_key_arn != null ? var.kms_key_arn : "" }) } @@ -127,7 +139,8 @@ resource "aws_iam_role_policy" "webhook_ssm" { role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/policies/lambda-ssm.json", { - github_app_webhook_secret_arn = var.github_app_parameters.webhook_secret.arn, + github_app_webhook_secret_arn = var.github_app_parameters.webhook_secret.arn, + parameter_runner_matcher_config_arn = aws_ssm_parameter.runner_matcher_config.arn }) } diff --git a/variables.tf b/variables.tf index 87a81bc5f0..8bb5cfa28d 100644 --- a/variables.tf +++ b/variables.tf @@ -773,6 +773,7 @@ variable "ssm_paths" { root = optional(string, "github-action-runners") app = optional(string, "app") runners = optional(string, "runners") + webhook = optional(string, "webhook") use_prefix = optional(bool, true) }) default = {}