Skip to content
This repository was archived by the owner on Jan 16, 2025. It is now read-only.

Commit 405b11d

Browse files
authored
feat: Strict label check and replace disable_check_wokflow_job_labels by opt in enable_workflow_job_labels_check (#1591)
* Check strict labels * feat: Replace disable_check_wokflow_job_labels by opt in enable_workflow_job_labels_check, and check all labels. * Make check strict * update docs * cleanup
1 parent 27e974d commit 405b11d

File tree

16 files changed

+233
-124
lines changed

16 files changed

+233
-124
lines changed

Diff for: README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Besides these permissions, the lambdas also need permission to CloudWatch (for l
8383
To be able to support a number of use-cases the module has quite a lot configuration options. We try to choose reasonable defaults. The several examples also shows for the main cases how to configure the runners.
8484

8585
- Org vs Repo level. You can configure the module to connect the runners in GitHub on a org level and share the runners in your org. Or set the runners on repo level. The module will install the runner to the repo. This can be multiple repo's but runners are not shared between repo's.
86-
- Checkrun vs Workflow job event. You can configure the webhook in GitHub to send checkrun or workflow job events to the webhook. Workflow job events are introduced by GitHub in September 2021 and are designed to support scalable runners. We advise when possible to use the workflow job event, you can set `disable_check_wokflow_job_labels = true` to disable the label check.
86+
- Checkrun vs Workflow job event. You can configure the webhook in GitHub to send checkrun or workflow job events to the webhook. Workflow job events are introduced by GitHub in September 2021 and are designed to support scalable runners. We advise when possible to use the workflow job event, you can set `runner_enable_workflow_job_labels_check = true` to let the webhook only accept jobs based on the labels configured. The webhook will check the custom labels provided via the variable `runner_extra_labels` and the GitHub managed labels, "self-hosted", OS and architecture. The OS and architecture are derived from the settings. By default the check is disabled.
8787
- Linux vs Windows. you can configure the os types linux and win. Linux will be used by default.
8888
- Re-use vs Ephemeral. By default runners are re-used for till detected idle, once idle they will be removed from the pool. To improve security we are introducing ephemeral runners. Those runners are only used for one job. Ephemeral runners are only working in combination with the workflow job event. We also suggest to use a pre-build AMI to improve the start time of jobs.
8989
- GitHub cloud vs GitHub enterprise server (GHES). The runner support GitHub cloud as well GitHub enterprise service. For GHES we rely on our community to test and support. We have no possibility to test ourselves on GHES.
@@ -382,7 +382,6 @@ In case the setup does not work as intended follow the trace of events:
382382
| <a name="input_cloudwatch_config"></a> [cloudwatch\_config](#input\_cloudwatch\_config) | (optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details. | `string` | `null` | no |
383383
| <a name="input_create_service_linked_role_spot"></a> [create\_service\_linked\_role\_spot](#input\_create\_service\_linked\_role\_spot) | (optional) create the serviced linked role for spot instances that is required by the scale-up lambda. | `bool` | `false` | no |
384384
| <a name="input_delay_webhook_event"></a> [delay\_webhook\_event](#input\_delay\_webhook\_event) | The number of seconds the event accepted by the webhook is invisible on the queue before the scale up lambda will receive the event. | `number` | `30` | no |
385-
| <a name="input_disable_check_wokflow_job_labels"></a> [disable\_check\_wokflow\_job\_labels](#input\_disable\_check\_wokflow\_job\_labels) | Disable the the check of workflow labels for received workflow job events. | `bool` | `false` | no |
386385
| <a name="input_enable_cloudwatch_agent"></a> [enable\_cloudwatch\_agent](#input\_enable\_cloudwatch\_agent) | Enabling the cloudwatch agent on the ec2 runner instances, the runner contains default config. Configuration can be overridden via `cloudwatch_config`. | `bool` | `true` | no |
387386
| <a name="input_enable_ephemeral_runners"></a> [enable\_ephemeral\_runners](#input\_enable\_ephemeral\_runners) | Enable ephemeral runners, runners will only be used once. | `bool` | `false` | no |
388387
| <a name="input_enable_organization_runners"></a> [enable\_organization\_runners](#input\_enable\_organization\_runners) | Register runners to organization, instead of repo level | `bool` | `false` | no |
@@ -426,7 +425,8 @@ In case the setup does not work as intended follow the trace of events:
426425
| <a name="input_runner_boot_time_in_minutes"></a> [runner\_boot\_time\_in\_minutes](#input\_runner\_boot\_time\_in\_minutes) | The minimum time for an EC2 runner to boot and register as a runner. | `number` | `5` | no |
427426
| <a name="input_runner_ec2_tags"></a> [runner\_ec2\_tags](#input\_runner\_ec2\_tags) | Map of tags that will be added to the launch template instance tag specificatons. | `map(string)` | `{}` | no |
428427
| <a name="input_runner_egress_rules"></a> [runner\_egress\_rules](#input\_runner\_egress\_rules) | List of egress rules for the GitHub runner instances. | <pre>list(object({<br> cidr_blocks = list(string)<br> ipv6_cidr_blocks = list(string)<br> prefix_list_ids = list(string)<br> from_port = number<br> protocol = string<br> security_groups = list(string)<br> self = bool<br> to_port = number<br> description = string<br> }))</pre> | <pre>[<br> {<br> "cidr_blocks": [<br> "0.0.0.0/0"<br> ],<br> "description": null,<br> "from_port": 0,<br> "ipv6_cidr_blocks": [<br> "::/0"<br> ],<br> "prefix_list_ids": null,<br> "protocol": "-1",<br> "security_groups": null,<br> "self": null,<br> "to_port": 0<br> }<br>]</pre> | no |
429-
| <a name="input_runner_extra_labels"></a> [runner\_extra\_labels](#input\_runner\_extra\_labels) | Extra labels for the runners (GitHub). Separate each label by a comma | `string` | `""` | no |
428+
| <a name="input_runner_enable_workflow_job_labels_check"></a> [runner\_enable\_workflow\_job\_labels\_check](#input\_runner\_enable\_workflow\_job\_labels\_check) | If set to true all labels in the workflow job even are matched agaist the custom labels and GitHub labels (os, architecture and `self-hosted`). When the labels are not matching the event is dropped at the webhook. | `bool` | `false` | no |
429+
| <a name="input_runner_extra_labels"></a> [runner\_extra\_labels](#input\_runner\_extra\_labels) | Extra (custom) labels for the runners (GitHub). Separate each label by a comma. Labels checks on the webhook can be enforced by setting `enable_workflow_job_labels_check`. GitHub read-only labels should not be provided. | `string` | `""` | no |
430430
| <a name="input_runner_group_name"></a> [runner\_group\_name](#input\_runner\_group\_name) | Name of the runner group. | `string` | `"Default"` | no |
431431
| <a name="input_runner_iam_role_managed_policy_arns"></a> [runner\_iam\_role\_managed\_policy\_arns](#input\_runner\_iam\_role\_managed\_policy\_arns) | Attach AWS or customer-managed IAM policies (by ARN) to the runner IAM role | `list(string)` | `[]` | no |
432432
| <a name="input_runner_log_files"></a> [runner\_log\_files](#input\_runner\_log\_files) | (optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details. | <pre>list(object({<br> log_group_name = string<br> prefix_log_group = bool<br> file_path = string<br> log_stream_name = string<br> }))</pre> | `null` | no |

Diff for: examples/ephemeral/main.tf

+7-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ module "runners" {
3535
enable_organization_runners = true
3636
runner_extra_labels = "default,example"
3737

38+
# enable workflow labels check
39+
# runner_enable_workflow_job_labels_check = true
40+
3841
# enable access to the runners via SSM
3942
enable_ssm_on_runners = true
4043

@@ -55,12 +58,12 @@ module "runners" {
5558
enable_ephemeral_runners = true
5659

5760
# configure your pre-built AMI
58-
# enabled_userdata = false
59-
# ami_filter = { name = ["github-runner-amzn2-x86_64-2021*"] }
60-
# ami_owners = [data.aws_caller_identity.current.account_id]
61+
enabled_userdata = false
62+
ami_filter = { name = ["github-runner-amzn2-x86_64-2021*"] }
63+
ami_owners = [data.aws_caller_identity.current.account_id]
6164

6265
# Enable logging
63-
# log_level = "debug"
66+
log_level = "debug"
6467

6568
# Setup a dead letter queue, by default scale up lambda will kepp retrying to process event in case of scaling error.
6669
# redrive_policy_build_queue = {

Diff for: examples/windows/main.tf

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module "runners" {
3131
runner_extra_labels = "default,example"
3232

3333
# Set the OS to Windows
34-
runner_os = "win"
34+
runner_os = "windows"
3535
# we need to give the runner time to start because this is windows.
3636
runner_boot_time_in_minutes = 20
3737

Diff for: main.tf

+4-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ module "webhook" {
6767
lambda_zip = var.webhook_lambda_zip
6868
lambda_timeout = var.webhook_lambda_timeout
6969
logging_retention_in_days = var.logging_retention_in_days
70-
runner_extra_labels = var.runner_extra_labels
71-
disable_check_wokflow_job_labels = var.disable_check_wokflow_job_labels
70+
71+
# labels
72+
enable_workflow_job_labels_check = var.runner_enable_workflow_job_labels_check
73+
runner_labels = "self-hosted,${var.runner_os},${var.runner_architecture},${var.runner_extra_labels}"
7274

7375
role_path = var.role_path
7476
role_permissions_boundary = var.role_permissions_boundary

Diff for: modules/runners/logging.tf

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ locals {
1212
{
1313
"log_group_name" : "user_data",
1414
"prefix_log_group" : true,
15-
"file_path" : var.runner_os == "win" ? "C:/UserData.log" : "/var/log/user-data.log",
15+
"file_path" : var.runner_os == "windows" ? "C:/UserData.log" : "/var/log/user-data.log",
1616
"log_stream_name" : "{instance_id}"
1717
},
1818
{
1919
"log_group_name" : "runner",
2020
"prefix_log_group" : true,
21-
"file_path" : var.runner_os == "win" ? "C:/actions-runner/_diag/Runner_*.log" : "/home/runners/actions-runner/_diag/Runner_**.log",
21+
"file_path" : var.runner_os == "windows" ? "C:/actions-runner/_diag/Runner_*.log" : "/home/runners/actions-runner/_diag/Runner_**.log",
2222
"log_stream_name" : "{instance_id}"
2323
},
2424
{
2525
"log_group_name" : "runner-startup",
2626
"prefix_log_group" : true,
27-
"file_path" : var.runner_os == "win" ? "C:/runner-startup.log" : "/var/log/runner-startup.log",
27+
"file_path" : var.runner_os == "windows" ? "C:/runner-startup.log" : "/var/log/runner-startup.log",
2828
"log_stream_name" : "{instance_id}"
2929
}
3030
]

Diff for: modules/runners/main.tf

+8-8
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,23 @@ locals {
1616
kms_key_arn = var.kms_key_arn != null ? var.kms_key_arn : ""
1717

1818
default_ami = {
19-
"win" = { name = ["Windows_Server-20H2-English-Core-ContainersLatest-*"] }
20-
"linux" = var.runner_architecture == "arm64" ? { name = ["amzn2-ami-hvm-2*-arm64-gp2"] } : { name = ["amzn2-ami-hvm-2.*-x86_64-ebs"] }
19+
"windows" = { name = ["Windows_Server-20H2-English-Core-ContainersLatest-*"] }
20+
"linux" = var.runner_architecture == "arm64" ? { name = ["amzn2-ami-hvm-2*-arm64-gp2"] } : { name = ["amzn2-ami-hvm-2.*-x86_64-ebs"] }
2121
}
2222

2323
default_userdata_template = {
24-
"win" = "${path.module}/templates/user-data.ps1"
25-
"linux" = "${path.module}/templates/user-data.sh"
24+
"windows" = "${path.module}/templates/user-data.ps1"
25+
"linux" = "${path.module}/templates/user-data.sh"
2626
}
2727

2828
userdata_install_runner = {
29-
"win" = "${path.module}/templates/install-runner.ps1"
30-
"linux" = "${path.module}/templates/install-runner.sh"
29+
"windows" = "${path.module}/templates/install-runner.ps1"
30+
"linux" = "${path.module}/templates/install-runner.sh"
3131
}
3232

3333
userdata_start_runner = {
34-
"win" = "${path.module}/templates/start-runner.ps1"
35-
"linux" = "${path.module}/templates/start-runner.sh"
34+
"windows" = "${path.module}/templates/start-runner.ps1"
35+
"linux" = "${path.module}/templates/start-runner.sh"
3636
}
3737

3838
ami_filter = coalesce(var.ami_filter, local.default_ami[var.runner_os])

Diff for: modules/runners/scale-down.tf

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
locals {
22
# Windows Runners can take their sweet time to do anything
33
min_runtime_defaults = {
4-
"win" = 15
5-
"linux" = 5
4+
"windows" = 15
5+
"linux" = 5
66
}
77
}
88
resource "aws_lambda_function" "scale_down" {

Diff for: modules/runners/variables.tf

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ variable "runner_os" {
9696
default = "linux"
9797

9898
validation {
99-
condition = contains(["linux", "win"], var.runner_os)
99+
condition = contains(["linux", "windows"], var.runner_os)
100100
error_message = "Valid values for runner_os are (linux, win)."
101101
}
102102
}

Diff for: modules/webhook/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ No modules.
7474
|------|-------------|------|---------|:--------:|
7575
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | AWS region. | `string` | n/a | yes |
7676
| <a name="input_disable_check_wokflow_job_labels"></a> [disable\_check\_wokflow\_job\_labels](#input\_disable\_check\_wokflow\_job\_labels) | Disable the the check of workflow labels. | `bool` | `false` | no |
77+
| <a name="input_enable_workflow_job_labels_check"></a> [enable\_workflow\_job\_labels\_check](#input\_enable\_workflow\_job\_labels\_check) | If set to true all labels in the workflow job even are matched agaist the custom labels and GitHub labels (os, architecture and `self-hosted`). When the labels are not matching the event is dropped at the webhook. | `bool` | `false` | no |
7778
| <a name="input_environment"></a> [environment](#input\_environment) | A name that identifies the environment, used as prefix and for tagging. | `string` | n/a | yes |
7879
| <a name="input_github_app_webhook_secret_arn"></a> [github\_app\_webhook\_secret\_arn](#input\_github\_app\_webhook\_secret\_arn) | n/a | `string` | n/a | yes |
7980
| <a name="input_kms_key_arn"></a> [kms\_key\_arn](#input\_kms\_key\_arn) | Optional CMK Key ARN to be used for Parameter Store. | `string` | `null` | no |
@@ -86,7 +87,7 @@ No modules.
8687
| <a name="input_repository_white_list"></a> [repository\_white\_list](#input\_repository\_white\_list) | List of repositories allowed to use the github app | `list(string)` | `[]` | no |
8788
| <a name="input_role_path"></a> [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 |
8889
| <a name="input_role_permissions_boundary"></a> [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no |
89-
| <a name="input_runner_extra_labels"></a> [runner\_extra\_labels](#input\_runner\_extra\_labels) | Extra labels for the runners (GitHub). Separate each label by a comma | `string` | `""` | no |
90+
| <a name="input_runner_labels"></a> [runner\_labels](#input\_runner\_labels) | Labels for the runners (GitHub). Separate each label by a comma. Labels are used to check events when `runner_enable_workflow_job_labels_check` is set to `true`. | `string` | `""` | no |
9091
| <a name="input_sqs_build_queue"></a> [sqs\_build\_queue](#input\_sqs\_build\_queue) | SQS queue to publish accepted build events. | <pre>object({<br> id = string<br> arn = string<br> })</pre> | n/a | yes |
9192
| <a name="input_sqs_build_queue_fifo"></a> [sqs\_build\_queue\_fifo](#input\_sqs\_build\_queue\_fifo) | Enable a FIFO queue to remain the order of events received by the webhook. Suggest to set to true for repo level runners. | `bool` | `false` | no |
9293
| <a name="input_tags"></a> [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 |

Diff for: modules/webhook/lambdas/webhook/src/lambda.test.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { APIGatewayEvent, Context } from 'aws-lambda';
2+
import { mocked } from 'ts-jest/utils';
3+
import { githubWebhook } from './lambda';
4+
import { handle } from './webhook/handler';
5+
import { logger } from './webhook/logger';
6+
7+
const event: APIGatewayEvent = {
8+
body: JSON.stringify(''),
9+
headers: { abc: undefined },
10+
httpMethod: '',
11+
isBase64Encoded: false,
12+
multiValueHeaders: { abc: undefined },
13+
multiValueQueryStringParameters: null,
14+
path: '',
15+
pathParameters: null,
16+
queryStringParameters: null,
17+
stageVariables: null,
18+
resource: '',
19+
requestContext: {
20+
authorizer: null,
21+
accountId: '123456789012',
22+
resourceId: '123456',
23+
stage: 'prod',
24+
requestId: 'c6af9ac6-7b61-11e6-9a41-93e8deadbeef',
25+
requestTime: '09/Apr/2015:12:34:56 +0000',
26+
requestTimeEpoch: 1428582896000,
27+
identity: {
28+
cognitoIdentityPoolId: null,
29+
accountId: null,
30+
cognitoIdentityId: null,
31+
caller: null,
32+
accessKey: null,
33+
sourceIp: '127.0.0.1',
34+
cognitoAuthenticationType: null,
35+
cognitoAuthenticationProvider: null,
36+
userArn: null,
37+
userAgent: 'Custom User Agent String',
38+
user: null,
39+
clientCert: null,
40+
apiKey: null,
41+
apiKeyId: null,
42+
principalOrgId: null,
43+
},
44+
path: '/prod/path/to/resource',
45+
resourcePath: '/{proxy+}',
46+
httpMethod: 'POST',
47+
apiId: '1234567890',
48+
protocol: 'HTTP/1.1',
49+
},
50+
};
51+
52+
const context: Context = {
53+
awsRequestId: '1',
54+
callbackWaitsForEmptyEventLoop: false,
55+
functionName: '',
56+
functionVersion: '',
57+
getRemainingTimeInMillis: () => 0,
58+
invokedFunctionArn: '',
59+
logGroupName: '',
60+
logStreamName: '',
61+
memoryLimitInMB: '',
62+
done: () => {
63+
return;
64+
},
65+
fail: () => {
66+
return;
67+
},
68+
succeed: () => {
69+
return;
70+
},
71+
};
72+
73+
jest.mock('./webhook/handler');
74+
75+
describe('Test scale up lambda wrapper.', () => {
76+
it('Happy flow, resolve.', async () => {
77+
const mock = mocked(handle);
78+
mock.mockImplementation(() => {
79+
return new Promise((resolve) => {
80+
resolve({ statusCode: 200 });
81+
});
82+
});
83+
84+
const result = await githubWebhook(event, context);
85+
expect(result).toEqual({ statusCode: 200 });
86+
});
87+
88+
it('An expected error, resolve.', async () => {
89+
const mock = mocked(handle);
90+
mock.mockImplementation(() => {
91+
return new Promise((resolve) => {
92+
resolve({ statusCode: 400 });
93+
});
94+
});
95+
96+
const result = await githubWebhook(event, context);
97+
expect(result).toEqual({ statusCode: 400 });
98+
});
99+
100+
it('Errors are not thrown.', async () => {
101+
const mock = mocked(handle);
102+
const logSpy = jest.spyOn(logger, 'error');
103+
mock.mockRejectedValue(new Error('some error'));
104+
const result = await githubWebhook(event, context);
105+
expect(result).toMatchObject({ statusCode: 500 });
106+
expect(logSpy).toBeCalledTimes(1);
107+
});
108+
});

Diff for: modules/webhook/lambdas/webhook/src/lambda.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ export interface Response {
66
statusCode: number;
77
body?: string;
88
}
9-
10-
export const githubWebhook = async (event: APIGatewayEvent, context: Context, callback: Callback): Promise<void> => {
9+
export async function githubWebhook(event: APIGatewayEvent, context: Context): Promise<Response> {
1110
logger.setSettings({ requestId: context.awsRequestId });
1211
logger.debug(JSON.stringify(event));
12+
let result: Response;
1313
try {
14-
const response = await handle(event.headers, event.body as string);
15-
callback(null, response);
14+
result = await handle(event.headers, event.body as string);
1615
} catch (e) {
17-
callback(e as Error);
16+
logger.error(e);
17+
result = {
18+
statusCode: 500,
19+
body: 'Check the Lambda logs for the error details.',
20+
};
1821
}
19-
};
22+
return result;
23+
}

0 commit comments

Comments
 (0)