Skip to content

Commit c8dfc22

Browse files
feat: allow use any dynamic label with prefix ghr-
1 parent f6054b3 commit c8dfc22

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
@@ -402,7 +402,7 @@ describe('scaleUp with GHES', () => {
402402
describe('Dynamic EC2 Configuration', () => {
403403
beforeEach(() => {
404404
process.env.ENABLE_ORGANIZATION_RUNNERS = 'true';
405-
process.env.ENABLE_DYNAMIC_EC2_CONFIG = 'true';
405+
process.env.ENABLE_DYNAMIC_LABELS = 'true';
406406
process.env.ENABLE_EPHEMERAL_RUNNERS = 'true';
407407
process.env.ENABLE_JOB_QUEUED_CHECK = 'false';
408408
process.env.RUNNER_LABELS = 'base-label';
@@ -497,8 +497,8 @@ describe('scaleUp with GHES', () => {
497497
);
498498
});
499499

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

503503
const testDataWithEc2Labels = [
504504
{

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

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise<stri
275275
const instanceTypes = process.env.INSTANCE_TYPES.split(',');
276276
const instanceTargetCapacityType = process.env.INSTANCE_TARGET_CAPACITY_TYPE;
277277
const ephemeralEnabled = yn(process.env.ENABLE_EPHEMERAL_RUNNERS, { default: false });
278-
const dynamicEc2ConfigEnabled = yn(process.env.ENABLE_DYNAMIC_EC2_CONFIG, { default: false });
278+
const dynamicLabelsEnabled = yn(process.env.ENABLE_DYNAMIC_LABELS, { default: false });
279279
const enableJitConfig = yn(process.env.ENABLE_JIT_CONFIG, { default: ephemeralEnabled });
280280
const disableAutoUpdate = yn(process.env.DISABLE_RUNNER_AUTOUPDATE, { default: false });
281281
const launchTemplateName = process.env.LAUNCH_TEMPLATE_NAME;
@@ -341,12 +341,12 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise<stri
341341
: `${payload.repositoryOwner}/${payload.repositoryName}`;
342342

343343
let key = runnerOwner;
344-
if (dynamicEc2ConfigEnabled && labels?.length) {
345-
const requestedDynamicEc2Config = labels.find((l) => l.startsWith('ghr-ec2-'))?.slice('ghr-ec2-'.length);
344+
if (dynamicLabelsEnabled && labels?.length) {
345+
const dynamicLabels = labels.find((l) => l.startsWith('ghr-'))?.slice('ghr-'.length);
346346

347-
if (requestedDynamicEc2Config) {
348-
const ec2Hash = ec2LabelsHash(labels);
349-
key = `${key}/${ec2Hash}`;
347+
if (dynamicLabels) {
348+
const dynamicLabelsHash = labelsHash(labels);
349+
key = `${key}/${dynamicLabelsHash}`;
350350
}
351351
}
352352

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

391391
let ec2OverrideConfig: Ec2OverrideConfig | undefined = undefined;
392392

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

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

398-
if (dynamicEC2Labels.length > 0) {
399-
// Append all EC2 labels to runnerLabels
400-
runnerLabels = runnerLabels ? `${runnerLabels},${dynamicEC2Labels.join(',')}` : dynamicEC2Labels.join(',');
399+
if (allDynamicLabels.length > 0) {
400+
runnerLabels = runnerLabels ? `${runnerLabels},${allDynamicLabels.join(',')}` : allDynamicLabels.join(',');
401401

402402
logger.debug('Updated runner labels', { runnerLabels });
403403

404-
// Parse EC2 override configuration from labels
405-
ec2OverrideConfig = parseEc2OverrideConfig(dynamicEC2Labels);
406-
if (ec2OverrideConfig) {
407-
logger.debug('EC2 override config parsed from labels', {
408-
ec2OverrideConfig,
409-
});
404+
if (dynamicEC2Labels.length > 0) {
405+
406+
ec2OverrideConfig = parseEc2OverrideConfig(dynamicEC2Labels);
407+
if (ec2OverrideConfig) {
408+
logger.debug('EC2 override config parsed from labels', {
409+
ec2OverrideConfig,
410+
});
411+
}
410412
}
411413
} else {
412-
logger.debug('No dynamic EC2 labels found on message');
414+
logger.debug('No dynamic labels found on message');
413415
}
414416
}
415417

@@ -960,8 +962,8 @@ export function parseEc2OverrideConfig(labels: string[]): Ec2OverrideConfig | un
960962
return Object.keys(config).length > 0 ? config : undefined;
961963
}
962964

963-
function ec2LabelsHash(labels: string[]): string {
964-
const prefix = 'ghr-ec2-';
965+
function labelsHash(labels: string[]): string {
966+
const prefix = 'ghr-';
965967

966968
const input = labels
967969
.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"
@@ -754,3 +753,9 @@ variable "lambda_event_source_mapping_maximum_batching_window_in_seconds" {
754753
type = number
755754
default = 0
756755
}
756+
757+
variable "enable_dynamic_labels" {
758+
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."
759+
type = bool
760+
default = false
761+
}

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)