Skip to content

Commit 4090b85

Browse files
feat: allow use any dynamic label with prefix ghr-
1 parent a57a0da commit 4090b85

File tree

19 files changed

+87
-44
lines changed

19 files changed

+87
-44
lines changed

lambdas/functions/control-plane/src/scale-runners/scale-up.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ describe('scaleUp with GHES', () => {
575575
describe('Dynamic EC2 Configuration', () => {
576576
beforeEach(() => {
577577
process.env.ENABLE_ORGANIZATION_RUNNERS = 'true';
578-
process.env.ENABLE_DYNAMIC_EC2_CONFIG = 'true';
578+
process.env.ENABLE_DYNAMIC_LABELS = 'true';
579579
process.env.ENABLE_EPHEMERAL_RUNNERS = 'true';
580580
process.env.ENABLE_JOB_QUEUED_CHECK = 'false';
581581
process.env.RUNNER_LABELS = 'base-label';
@@ -670,8 +670,8 @@ describe('scaleUp with GHES', () => {
670670
);
671671
});
672672

673-
it('does not process EC2 labels when ENABLE_DYNAMIC_EC2_CONFIG is disabled', async () => {
674-
process.env.ENABLE_DYNAMIC_EC2_CONFIG = 'false';
673+
it('does not process EC2 labels when ENABLE_DYNAMIC_LABELS is disabled', async () => {
674+
process.env.ENABLE_DYNAMIC_LABELS = 'false';
675675

676676
const testDataWithEc2Labels = [
677677
{

lambdas/functions/control-plane/src/scale-runners/scale-up.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise<stri
336336
const instanceTypes = process.env.INSTANCE_TYPES.split(',');
337337
const instanceTargetCapacityType = process.env.INSTANCE_TARGET_CAPACITY_TYPE;
338338
const ephemeralEnabled = yn(process.env.ENABLE_EPHEMERAL_RUNNERS, { default: false });
339-
const dynamicEc2ConfigEnabled = yn(process.env.ENABLE_DYNAMIC_EC2_CONFIG, { default: false });
339+
const dynamicLabelsEnabled = yn(process.env.ENABLE_DYNAMIC_LABELS, { default: false });
340340
const enableJitConfig = yn(process.env.ENABLE_JIT_CONFIG, { default: ephemeralEnabled });
341341
const disableAutoUpdate = yn(process.env.DISABLE_RUNNER_AUTOUPDATE, { default: false });
342342
const launchTemplateName = process.env.LAUNCH_TEMPLATE_NAME;
@@ -406,12 +406,12 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise<stri
406406
: `${payload.repositoryOwner}/${payload.repositoryName}`;
407407

408408
let key = runnerOwner;
409-
if (dynamicEc2ConfigEnabled && labels?.length) {
410-
const requestedDynamicEc2Config = labels.find((l) => l.startsWith('ghr-ec2-'))?.slice('ghr-ec2-'.length);
409+
if (dynamicLabelsEnabled && labels?.length) {
410+
const dynamicLabels = labels.find((l) => l.startsWith('ghr-'))?.slice('ghr-'.length);
411411

412-
if (requestedDynamicEc2Config) {
413-
const ec2Hash = ec2LabelsHash(labels);
414-
key = `${key}/${ec2Hash}`;
412+
if (dynamicLabels) {
413+
const dynamicLabelsHash = labelsHash(labels);
414+
key = `${key}/${dynamicLabelsHash}`;
415415
}
416416
}
417417

@@ -456,26 +456,28 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise<stri
456456

457457
let ec2OverrideConfig: Ec2OverrideConfig | undefined = undefined;
458458

459-
if (messages.length > 0 && dynamicEc2ConfigEnabled) {
459+
if (messages.length > 0 && dynamicLabelsEnabled) {
460460
logger.debug('Dynamic EC2 config enabled, processing labels', { labels: messages[0].labels });
461461

462462
const dynamicEC2Labels = messages[0].labels?.map((l) => l.trim()).filter((l) => l.startsWith('ghr-ec2-')) ?? [];
463+
const allDynamicLabels = messages[0].labels?.map((l) => l.trim()).filter((l) => l.startsWith('ghr-')) ?? [];
463464

464-
if (dynamicEC2Labels.length > 0) {
465-
// Append all EC2 labels to runnerLabels
466-
runnerLabels = runnerLabels ? `${runnerLabels},${dynamicEC2Labels.join(',')}` : dynamicEC2Labels.join(',');
465+
if (allDynamicLabels.length > 0) {
466+
runnerLabels = runnerLabels ? `${runnerLabels},${allDynamicLabels.join(',')}` : allDynamicLabels.join(',');
467467

468468
logger.debug('Updated runner labels', { runnerLabels });
469469

470-
// Parse EC2 override configuration from labels
471-
ec2OverrideConfig = parseEc2OverrideConfig(dynamicEC2Labels);
472-
if (ec2OverrideConfig) {
473-
logger.debug('EC2 override config parsed from labels', {
474-
ec2OverrideConfig,
475-
});
470+
if (dynamicEC2Labels.length > 0) {
471+
472+
ec2OverrideConfig = parseEc2OverrideConfig(dynamicEC2Labels);
473+
if (ec2OverrideConfig) {
474+
logger.debug('EC2 override config parsed from labels', {
475+
ec2OverrideConfig,
476+
});
477+
}
476478
}
477479
} else {
478-
logger.debug('No dynamic EC2 labels found on message');
480+
logger.debug('No dynamic labels found on message');
479481
}
480482
}
481483

@@ -1085,8 +1087,8 @@ export function parseEc2OverrideConfig(labels: string[]): Ec2OverrideConfig | un
10851087
return Object.keys(config).length > 0 ? config : undefined;
10861088
}
10871089

1088-
function ec2LabelsHash(labels: string[]): string {
1089-
const prefix = 'ghr-ec2-';
1090+
function labelsHash(labels: string[]): string {
1091+
const prefix = 'ghr-';
10901092

10911093
const input = labels
10921094
.filter((l) => l.startsWith(prefix))

lambdas/functions/webhook/src/ConfigLoader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ export class ConfigWebhook extends MatcherAwareConfig {
119119
repositoryAllowList: string[] = [];
120120
webhookSecret: string = '';
121121
workflowJobEventSecondaryQueue: string = '';
122+
enableDynamicLabels: boolean = false;
122123

123124
async loadConfig(): Promise<void> {
124125
this.loadEnvVar(process.env.REPOSITORY_ALLOW_LIST, 'repositoryAllowList', []);
126+
this.loadEnvVar(process.env.ENABLE_DYNAMIC_LABELS, 'enableDynamicLabels', false);
125127

126128
await Promise.all([
127129
this.loadMatcherConfig(process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH),
@@ -151,9 +153,11 @@ export class ConfigWebhookEventBridge extends BaseConfig {
151153
export class ConfigDispatcher extends MatcherAwareConfig {
152154
repositoryAllowList: string[] = [];
153155
workflowJobEventSecondaryQueue: string = ''; // Deprecated
156+
enableDynamicLabels: boolean = false;
154157

155158
async loadConfig(): Promise<void> {
156159
this.loadEnvVar(process.env.REPOSITORY_ALLOW_LIST, 'repositoryAllowList', []);
160+
this.loadEnvVar(process.env.ENABLE_DYNAMIC_LABELS, 'enableDynamicLabels', false);
157161
await this.loadMatcherConfig(process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH);
158162

159163
validateRunnerMatcherConfig(this);

lambdas/functions/webhook/src/modules.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ declare namespace NodeJS {
55
PARAMETER_GITHUB_APP_WEBHOOK_SECRET: string;
66
PARAMETER_RUNNER_MATCHER_CONFIG_PATH: string;
77
REPOSITORY_ALLOW_LIST: string;
8+
ENABLE_DYNAMIC_LABELS: string
89
RUNNER_LABELS: string;
910
ACCEPT_EVENTS: string;
1011
}

lambdas/functions/webhook/src/runners/dispatch.test.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,49 +183,61 @@ describe('Dispatcher', () => {
183183
it('should accept job with an exact match and identical labels.', () => {
184184
const workflowLabels = ['self-hosted', 'linux', 'x64', 'ubuntu-latest'];
185185
const runnerLabels = [['self-hosted', 'linux', 'x64', 'ubuntu-latest']];
186-
expect(canRunJob(workflowLabels, runnerLabels, true)).toBe(true);
186+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(true);
187187
});
188188

189189
it('should accept job with an exact match and identical labels, ignoring cases.', () => {
190190
const workflowLabels = ['self-Hosted', 'Linux', 'X64', 'ubuntu-Latest'];
191191
const runnerLabels = [['self-hosted', 'linux', 'x64', 'ubuntu-latest']];
192-
expect(canRunJob(workflowLabels, runnerLabels, true)).toBe(true);
192+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(true);
193193
});
194194

195195
it('should accept job with an exact match and runner supports requested capabilities.', () => {
196196
const workflowLabels = ['self-hosted', 'linux', 'x64'];
197197
const runnerLabels = [['self-hosted', 'linux', 'x64', 'ubuntu-latest']];
198-
expect(canRunJob(workflowLabels, runnerLabels, true)).toBe(true);
198+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(true);
199199
});
200200

201201
it('should NOT accept job with an exact match and runner not matching requested capabilities.', () => {
202202
const workflowLabels = ['self-hosted', 'linux', 'x64', 'ubuntu-latest'];
203203
const runnerLabels = [['self-hosted', 'linux', 'x64']];
204-
expect(canRunJob(workflowLabels, runnerLabels, true)).toBe(false);
204+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(false);
205205
});
206206

207207
it('should accept job with for a non exact match. Any label that matches will accept the job.', () => {
208208
const workflowLabels = ['self-hosted', 'linux', 'x64', 'ubuntu-latest', 'gpu'];
209209
const runnerLabels = [['gpu']];
210-
expect(canRunJob(workflowLabels, runnerLabels, false)).toBe(true);
210+
expect(canRunJob(workflowLabels, runnerLabels, false, false)).toBe(true);
211211
});
212212

213213
it('should NOT accept job with for an exact match. Not all requested capabilities are supported.', () => {
214214
const workflowLabels = ['self-hosted', 'linux', 'x64', 'ubuntu-latest', 'gpu'];
215215
const runnerLabels = [['gpu']];
216-
expect(canRunJob(workflowLabels, runnerLabels, true)).toBe(false);
216+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(false);
217217
});
218218

219219
it('should not accept jobs not providing labels if exact match is.', () => {
220220
const workflowLabels: string[] = [];
221221
const runnerLabels = [['self-hosted', 'linux', 'x64']];
222-
expect(canRunJob(workflowLabels, runnerLabels, true)).toBe(false);
222+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(false);
223223
});
224224

225225
it('should accept jobs not providing labels and exact match is set to false.', () => {
226226
const workflowLabels: string[] = [];
227227
const runnerLabels = [['self-hosted', 'linux', 'x64']];
228-
expect(canRunJob(workflowLabels, runnerLabels, false)).toBe(true);
228+
expect(canRunJob(workflowLabels, runnerLabels, false, false)).toBe(true);
229+
});
230+
231+
it('should filter out ghr- and ghr-run- labels when enableDynamicLabels is true.', () => {
232+
const workflowLabels = ['self-hosted', 'linux', 'x64', 'ghr-ec2-instance-type:t3.large', 'ghr-run-id:12345'];
233+
const runnerLabels = [['self-hosted', 'linux', 'x64']];
234+
expect(canRunJob(workflowLabels, runnerLabels, true, true)).toBe(true);
235+
});
236+
237+
it('should NOT filter out ghr- and ghr-run- labels when enableDynamicLabels is false.', () => {
238+
const workflowLabels = ['self-hosted', 'linux', 'x64', 'ghr-ec2-instance-type:t3.large'];
239+
const runnerLabels = [['self-hosted', 'linux', 'x64']];
240+
expect(canRunJob(workflowLabels, runnerLabels, true, false)).toBe(false);
229241
});
230242
});
231243
});

lambdas/functions/webhook/src/runners/dispatch.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function dispatch(
1515
): Promise<Response> {
1616
validateRepoInAllowList(event, config);
1717

18-
return await handleWorkflowJob(event, eventType, config.matcherConfig!);
18+
return await handleWorkflowJob(event, eventType, config.matcherConfig!, config.enableDynamicLabels);
1919
}
2020

2121
function validateRepoInAllowList(event: WorkflowJobEvent, config: ConfigDispatcher) {
@@ -29,6 +29,7 @@ async function handleWorkflowJob(
2929
body: WorkflowJobEvent,
3030
githubEvent: string,
3131
matcherConfig: Array<RunnerMatcherConfig>,
32+
enableDynamicLabels: boolean,
3233
): Promise<Response> {
3334
if (body.action !== 'queued') {
3435
return {
@@ -47,7 +48,7 @@ async function handleWorkflowJob(
4748
return a.matcherConfig.exactMatch === b.matcherConfig.exactMatch ? 0 : a.matcherConfig.exactMatch ? -1 : 1;
4849
});
4950
for (const queue of matcherConfig) {
50-
if (canRunJob(body.workflow_job.labels, queue.matcherConfig.labelMatchers, queue.matcherConfig.exactMatch)) {
51+
if (canRunJob(body.workflow_job.labels, queue.matcherConfig.labelMatchers, queue.matcherConfig.exactMatch, enableDynamicLabels)) {
5152
await sendActionRequest({
5253
id: body.workflow_job.id,
5354
repositoryName: body.repository.name,
@@ -81,9 +82,12 @@ export function canRunJob(
8182
workflowJobLabels: string[],
8283
runnerLabelsMatchers: string[][],
8384
workflowLabelCheckAll: boolean,
85+
enableDynamicLabels: boolean,
8486
): boolean {
85-
// Filter out ghr-ec2- labels as they are handled by the dynamic EC2 instance type feature
86-
const filteredLabels = workflowJobLabels.filter((label) => !label.startsWith('ghr-ec2-'));
87+
// Filter out ghr- and ghr-run- labels only if dynamic labels config is enabled
88+
const filteredLabels = enableDynamicLabels
89+
? workflowJobLabels.filter((label) => !label.startsWith('ghr-'))
90+
: workflowJobLabels;
8791

8892
runnerLabelsMatchers = runnerLabelsMatchers.map((runnerLabel) => {
8993
return runnerLabel.map((label) => label.toLowerCase());

main.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ module "webhook" {
136136
tracing_config = var.tracing_config
137137
logging_retention_in_days = var.logging_retention_in_days
138138
logging_kms_key_id = var.logging_kms_key_id
139+
enable_dynamic_labels = var.enable_dynamic_labels
139140

140141
role_path = var.role_path
141142
role_permissions_boundary = var.role_permissions_boundary
@@ -184,7 +185,7 @@ module "runners" {
184185
github_app_parameters = local.github_app_parameters
185186
enable_organization_runners = var.enable_organization_runners
186187
enable_ephemeral_runners = var.enable_ephemeral_runners
187-
enable_dynamic_ec2_config = var.enable_dynamic_ec2_config
188+
enable_dynamic_labels = var.enable_dynamic_labels
188189
enable_job_queued_check = var.enable_job_queued_check
189190
enable_jit_config = var.enable_jit_config
190191
enable_on_demand_failover_for_errors = var.enable_runner_on_demand_failover_for_errors

modules/multi-runner/runners.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module "runners" {
3535
scale_errors = each.value.runner_config.scale_errors
3636
enable_organization_runners = each.value.runner_config.enable_organization_runners
3737
enable_ephemeral_runners = each.value.runner_config.enable_ephemeral_runners
38-
enable_dynamic_ec2_config = each.value.runner_config.enable_dynamic_ec2_config
38+
enable_dynamic_labels = each.value.runner_config.enable_dynamic_labels
3939
enable_jit_config = each.value.runner_config.enable_jit_config
4040
enable_job_queued_check = each.value.runner_config.enable_job_queued_check
4141
disable_runner_autoupdate = each.value.runner_config.disable_runner_autoupdate

modules/multi-runner/variables.tf

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ variable "multi_runner_config" {
7777
disable_runner_autoupdate = optional(bool, false)
7878
ebs_optimized = optional(bool, false)
7979
enable_ephemeral_runners = optional(bool, false)
80-
enable_dynamic_ec2_config = optional(bool, false)
8180
enable_job_queued_check = optional(bool, null)
8281
enable_on_demand_failover_for_errors = optional(list(string), [])
8382
scale_errors = optional(list(string), [
@@ -207,7 +206,7 @@ variable "multi_runner_config" {
207206
disable_runner_autoupdate: "Disable the auto update of the github runner agent. Be aware there is a grace period of 30 days, see also the [GitHub article](https://github.blog/changelog/2022-02-01-github-actions-self-hosted-runners-can-now-disable-automatic-updates/)"
208207
ebs_optimized: "The EC2 EBS optimized configuration."
209208
enable_ephemeral_runners: "Enable ephemeral runners, runners will only be used once."
210-
enable_dynamic_ec2_config: "Enable dynamic EC2 configs based on workflow job labels. When enabled, jobs can request specific configs via the 'gh-ec2-<config type key>:<config type value>' label (e.g., 'gh-ec2-instance-type:t3.large')."
209+
enable_dynamic_labels: "Enable dynamic labels with 'ghr-' prefix. When enabled, jobs can use 'ghr-ec2-<config>:<value>' labels to dynamically configure EC2 instances (e.g., 'ghr-ec2-instance-type:t3.large') and 'ghr-run-<label>' to add unique labels dynamically to runners."
211210
enable_job_queued_check: "(Optional) Only scale if the job event received by the scale up lambda is is in the state queued. By default enabled for non ephemeral runners and disabled for ephemeral. Set this variable to overwrite the default behavior."
212211
enable_on_demand_failover_for_errors: "Enable on-demand failover. For example to fall back to on demand when no spot capacity is available the variable can be set to `InsufficientInstanceCapacity`. When not defined the default behavior is to retry later."
213212
scale_errors: "List of aws error codes that should trigger retry during scale up. This list will replace the default errors defined in the variable `defaultScaleErrors` in https://github.com/github-aws-runners/terraform-aws-github-runner/blob/main/lambdas/functions/control-plane/src/aws/runners.ts"
@@ -760,3 +759,9 @@ variable "parameter_store_tags" {
760759
type = map(string)
761760
default = {}
762761
}
762+
763+
variable "enable_dynamic_labels" {
764+
description = "Enable dynamic labels with 'ghr-' prefix. When enabled, jobs can use 'ghr-ec2-<config>:<value>' labels to dynamically configure EC2 instances (e.g., 'ghr-ec2-instance-type:t3.large') and 'ghr-run-<label>' to add unique labels dynamically to runners."
765+
type = bool
766+
default = false
767+
}

modules/multi-runner/webhook.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,7 @@ module "webhook" {
3838
lambda_security_group_ids = var.lambda_security_group_ids
3939
aws_partition = var.aws_partition
4040

41+
enable_dynamic_labels = var.enable_dynamic_labels
42+
4143
log_level = var.log_level
4244
}

0 commit comments

Comments
 (0)