From 6033b2cb44fc229c6ebf681ecbd089163bdc2faa Mon Sep 17 00:00:00 2001 From: "rick.stokkingreef" Date: Thu, 30 Apr 2026 13:18:33 +0200 Subject: [PATCH 1/3] feat(helm): add $extra field passthrough for runner container in dind and kubernetes modes Mirror the pattern already used in mode-empty and the dind sidecar container: read runner.container as a map, validate known fields, and pass any unrecognised keys (e.g. securityContext, resources, imagePullPolicy) through to the rendered container spec via $extra = omit ... | toYaml. Includes input validation (map/list/unsupported-key guards) and 14 new helm-unittest test cases (7 per mode) covering passthrough, name-override suppression, and validation failure paths. --- .../templates/_mode_dind.tpl | 25 ++- .../templates/_mode_kubernetes.tpl | 25 ++- ...caling_runner_set_dind_mode_spec_test.yaml | 165 ++++++++++++++++++ ..._runner_set_kubernetes_mode_spec_test.yaml | 165 ++++++++++++++++++ 4 files changed, 378 insertions(+), 2 deletions(-) diff --git a/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl b/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl index a6e7ade5a2..b0806a6349 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl @@ -1,4 +1,23 @@ {{- define "runner-mode-dind.runner-container" -}} +{{- $container := (.Values.runner.container | default dict) -}} +{{- if and (hasKey .Values.runner "container") (not (kindIs "map" $container)) -}} + {{- fail "runner.container must be a map/object" -}} +{{- end -}} +{{- if and (hasKey $container "env") (not (kindIs "slice" $container.env)) -}} + {{- fail "runner.container.env must be a list" -}} +{{- end -}} +{{- if and (hasKey $container "volumeMounts") (not (kindIs "slice" $container.volumeMounts)) -}} + {{- fail "runner.container.volumeMounts must be a list" -}} +{{- end -}} +{{- if hasKey $container "volumes" -}} + {{- fail "runner.container.volumes is not supported; use runner.pod.spec.volumes" -}} +{{- end -}} +{{- if and (hasKey $container "args") (not (kindIs "slice" $container.args)) -}} + {{- fail "runner.container.args must be a list" -}} +{{- end -}} +{{- if and (hasKey $container "securityContext") (not (kindIs "map" $container.securityContext)) -}} + {{- fail "runner.container.securityContext must be a map/object" -}} +{{- end -}} name: runner image: {{ include "runner.image" . | quote }} command: {{ include "runner.command" . }} @@ -15,7 +34,11 @@ volumeMounts: mountPath: /home/runner/_work - name: dind-sock mountPath: {{ include "runner-mode-dind.sock-mount-dir" . | quote }} - {{ include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (list)) | nindent 2 }} + {{ include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (list)) | nindent 2 -}} +{{- $extra := omit $container "name" "image" "command" "env" "volumeMounts" -}} +{{- if not (empty $extra) }} +{{ toYaml $extra -}} +{{- end -}} {{- end }} {{- define "runner-mode-dind.dind-container" -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl b/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl index 6589d01d1c..d82e426892 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl @@ -1,5 +1,24 @@ {{- define "runner-mode-kubernetes.runner-container" -}} {{- $runner := (.Values.runner | default dict) -}} +{{- $container := (.Values.runner.container | default dict) -}} +{{- if and (hasKey $runner "container") (not (kindIs "map" $container)) -}} + {{- fail "runner.container must be a map/object" -}} +{{- end -}} +{{- if and (hasKey $container "env") (not (kindIs "slice" $container.env)) -}} + {{- fail "runner.container.env must be a list" -}} +{{- end -}} +{{- if and (hasKey $container "volumeMounts") (not (kindIs "slice" $container.volumeMounts)) -}} + {{- fail "runner.container.volumeMounts must be a list" -}} +{{- end -}} +{{- if hasKey $container "volumes" -}} + {{- fail "runner.container.volumes is not supported; use runner.pod.spec.volumes" -}} +{{- end -}} +{{- if and (hasKey $container "args") (not (kindIs "slice" $container.args)) -}} + {{- fail "runner.container.args must be a list" -}} +{{- end -}} +{{- if and (hasKey $container "securityContext") (not (kindIs "map" $container.securityContext)) -}} + {{- fail "runner.container.securityContext must be a map/object" -}} +{{- end -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} {{- $hookPath := (index $kubeMode "hookPath" | default "/home/runner/k8s/index.js") -}} {{- $extensionRef := (index $kubeMode "extensionRef" | default "") -}} @@ -82,7 +101,11 @@ volumeMounts: subPath: extension readOnly: true {{- end }} - {{ include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (list)) | nindent 2 }} + {{ include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (list)) | nindent 2 -}} +{{- $extra := omit $container "name" "image" "command" "env" "volumeMounts" -}} +{{- if not (empty $extra) }} +{{ toYaml $extra -}} +{{- end -}} {{- end }} {{- define "runner-mode-kubernetes.pod-volumes" -}} diff --git a/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_dind_mode_spec_test.yaml b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_dind_mode_spec_test.yaml index 2ec6d4dbc8..7832184ce3 100644 --- a/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_dind_mode_spec_test.yaml +++ b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_dind_mode_spec_test.yaml @@ -400,6 +400,171 @@ tests: name: cache mountPath: /cache + - it: should pass extra fields from runner.container to the runner container in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + securityContext: + runAsUser: 1000 + resources: + limits: + cpu: "250m" + memory: "64Mi" + imagePullPolicy: Always + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: runner + - equal: + path: spec.template.spec.containers[0].securityContext.runAsUser + value: 1000 + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: 250m + - equal: + path: spec.template.spec.containers[0].resources.limits.memory + value: 64Mi + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + + - it: should silently ignore runner.container.name in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + name: not-runner + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: runner + + - it: should fail when runner.container is not a map in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: "invalid" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container must be a map/object + + - it: should fail when runner.container.env is not a list in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + env: "not-a-list" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.env must be a list + + - it: should fail when runner.container.volumeMounts is not a list in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + volumeMounts: "not-a-list" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.volumeMounts must be a list + + - it: should fail when runner.container.volumes is set in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + volumes: + - name: cache + emptyDir: {} + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.volumes is not supported; use runner.pod.spec.volumes + + - it: should fail when runner.container.args is not a list in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + args: "not-a-list" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.args must be a list + + - it: should fail when runner.container.securityContext is not a map in dind mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "dind" + container: + securityContext: "not-a-map" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.securityContext must be a map/object + - it: should fail when runner.dind.container.volumes is provided set: scaleset.name: "test" diff --git a/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_kubernetes_mode_spec_test.yaml b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_kubernetes_mode_spec_test.yaml index 91429779e7..f571e1b724 100644 --- a/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_kubernetes_mode_spec_test.yaml +++ b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_kubernetes_mode_spec_test.yaml @@ -99,6 +99,171 @@ tests: path: spec.template.spec.volumes[0].ephemeral.volumeClaimTemplate.spec.resources.requests.storage value: 10Gi + - it: should pass extra fields from runner.container to the runner container in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + securityContext: + runAsUser: 1000 + resources: + limits: + cpu: "250m" + memory: "64Mi" + imagePullPolicy: Always + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: runner + - equal: + path: spec.template.spec.containers[0].securityContext.runAsUser + value: 1000 + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: 250m + - equal: + path: spec.template.spec.containers[0].resources.limits.memory + value: 64Mi + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + + - it: should silently ignore runner.container.name in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + name: not-runner + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: runner + + - it: should fail when runner.container is not a map in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: "invalid" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container must be a map/object + + - it: should fail when runner.container.env is not a list in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + env: "not-a-list" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.env must be a list + + - it: should fail when runner.container.volumeMounts is not a list in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + volumeMounts: "not-a-list" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.volumeMounts must be a list + + - it: should fail when runner.container.volumes is set in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + volumes: + - name: cache + emptyDir: {} + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.volumes is not supported; use runner.pod.spec.volumes + + - it: should fail when runner.container.args is not a list in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + args: "not-a-list" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.args must be a list + + - it: should fail when runner.container.securityContext is not a map in kubernetes mode + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + mode: "kubernetes" + container: + securityContext: "not-a-map" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: runner.container.securityContext must be a map/object + - it: should include extraVolumes in kubernetes mode set: scaleset.name: "test" From a0f8fa9ddb3636dbaecff7889f7fdad771176f83 Mon Sep 17 00:00:00 2001 From: "rick.stokkingreef" Date: Thu, 30 Apr 2026 13:21:13 +0200 Subject: [PATCH 2/3] docs(helm/values): document runner.container extra field passthrough Add commented examples for env, volumeMounts, args, securityContext, and resources under runner.container, mirroring the existing style of the runner.dind.container section. --- charts/gha-runner-scale-set-experimental/values.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/charts/gha-runner-scale-set-experimental/values.yaml b/charts/gha-runner-scale-set-experimental/values.yaml index 4984251dc2..dd5a678689 100644 --- a/charts/gha-runner-scale-set-experimental/values.yaml +++ b/charts/gha-runner-scale-set-experimental/values.yaml @@ -232,10 +232,16 @@ runner: initContainers: [] volumes: [] - # container field is applied to the container named "runner". You cannot override the name of the runner container + # container field is applied to the container named "runner". You cannot override the name of the runner container. + # Additional container fields are passed through as-is (e.g. resources, securityContext, imagePullPolicy, etc.) container: image: "ghcr.io/actions/actions-runner:latest" command: ["/home/runner/run.sh"] + # env: [] + # volumeMounts: [] + # args: [] + # securityContext: {} + # resources: {} dind: # If official runner image is used, or the dind image doesn't contain From 4c5dcbb73802004b2f4e0f93d8adc77acc3b06d4 Mon Sep 17 00:00:00 2001 From: "rick.stokkingreef" Date: Thu, 30 Apr 2026 15:16:51 +0200 Subject: [PATCH 3/3] refactor(helm): extract runner.container validation into shared helper Extract the six runner.container type-guard checks into a reusable named template 'runner.container.validate' in _defaults.tpl. Both _mode_dind.tpl and _mode_kubernetes.tpl now call this helper instead of duplicating the block, preventing future drift. Also fixes _mode_dind.tpl to use $runner := (.Values.runner | default dict) before accessing runner.container, making the failure mode consistent with kubernetes mode when runner itself is overridden to a non-map. --- .../templates/_defaults.tpl | 34 +++++++++++++++++++ .../templates/_mode_dind.tpl | 22 ++---------- .../templates/_mode_kubernetes.tpl | 21 ++---------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/charts/gha-runner-scale-set-experimental/templates/_defaults.tpl b/charts/gha-runner-scale-set-experimental/templates/_defaults.tpl index 055c75a202..2cecf588a1 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_defaults.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_defaults.tpl @@ -122,6 +122,40 @@ Hook extension ConfigMap name for kubernetes runner mode. If runner.kubernetesMode.extension.metadata.name is set, use it. Otherwise, default to a name derived from the scale set name. */}} +{{/* +Validate runner.container fields. + +Fails with a descriptive error if: +- runner.container is set but is not a map/object +- runner.container.env is set but is not a list +- runner.container.volumeMounts is set but is not a list +- runner.container.args is set but is not a list +- runner.container.securityContext is set but is not a map/object +- runner.container.volumes is set (unsupported; use runner.pod.spec.volumes) +*/}} +{{- define "runner.container.validate" -}} +{{- $runner := (.Values.runner | default dict) -}} +{{- $container := ($runner.container | default dict) -}} +{{- if and (hasKey $runner "container") (not (kindIs "map" $container)) -}} + {{- fail "runner.container must be a map/object" -}} +{{- end -}} +{{- if and (hasKey $container "env") (not (kindIs "slice" $container.env)) -}} + {{- fail "runner.container.env must be a list" -}} +{{- end -}} +{{- if and (hasKey $container "volumeMounts") (not (kindIs "slice" $container.volumeMounts)) -}} + {{- fail "runner.container.volumeMounts must be a list" -}} +{{- end -}} +{{- if hasKey $container "volumes" -}} + {{- fail "runner.container.volumes is not supported; use runner.pod.spec.volumes" -}} +{{- end -}} +{{- if and (hasKey $container "args") (not (kindIs "slice" $container.args)) -}} + {{- fail "runner.container.args must be a list" -}} +{{- end -}} +{{- if and (hasKey $container "securityContext") (not (kindIs "map" $container.securityContext)) -}} + {{- fail "runner.container.securityContext must be a map/object" -}} +{{- end -}} +{{- end -}} + {{- define "runner-mode-kubernetes.extension-name" -}} {{- $runner := (.Values.runner | default dict) -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl b/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl index b0806a6349..854acb9c84 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_mode_dind.tpl @@ -1,23 +1,7 @@ {{- define "runner-mode-dind.runner-container" -}} -{{- $container := (.Values.runner.container | default dict) -}} -{{- if and (hasKey .Values.runner "container") (not (kindIs "map" $container)) -}} - {{- fail "runner.container must be a map/object" -}} -{{- end -}} -{{- if and (hasKey $container "env") (not (kindIs "slice" $container.env)) -}} - {{- fail "runner.container.env must be a list" -}} -{{- end -}} -{{- if and (hasKey $container "volumeMounts") (not (kindIs "slice" $container.volumeMounts)) -}} - {{- fail "runner.container.volumeMounts must be a list" -}} -{{- end -}} -{{- if hasKey $container "volumes" -}} - {{- fail "runner.container.volumes is not supported; use runner.pod.spec.volumes" -}} -{{- end -}} -{{- if and (hasKey $container "args") (not (kindIs "slice" $container.args)) -}} - {{- fail "runner.container.args must be a list" -}} -{{- end -}} -{{- if and (hasKey $container "securityContext") (not (kindIs "map" $container.securityContext)) -}} - {{- fail "runner.container.securityContext must be a map/object" -}} -{{- end -}} +{{- include "runner.container.validate" . -}} +{{- $runner := (.Values.runner | default dict) -}} +{{- $container := ($runner.container | default dict) -}} name: runner image: {{ include "runner.image" . | quote }} command: {{ include "runner.command" . }} diff --git a/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl b/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl index d82e426892..e876b0a3e8 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_mode_kubernetes.tpl @@ -1,24 +1,7 @@ {{- define "runner-mode-kubernetes.runner-container" -}} +{{- include "runner.container.validate" . -}} {{- $runner := (.Values.runner | default dict) -}} -{{- $container := (.Values.runner.container | default dict) -}} -{{- if and (hasKey $runner "container") (not (kindIs "map" $container)) -}} - {{- fail "runner.container must be a map/object" -}} -{{- end -}} -{{- if and (hasKey $container "env") (not (kindIs "slice" $container.env)) -}} - {{- fail "runner.container.env must be a list" -}} -{{- end -}} -{{- if and (hasKey $container "volumeMounts") (not (kindIs "slice" $container.volumeMounts)) -}} - {{- fail "runner.container.volumeMounts must be a list" -}} -{{- end -}} -{{- if hasKey $container "volumes" -}} - {{- fail "runner.container.volumes is not supported; use runner.pod.spec.volumes" -}} -{{- end -}} -{{- if and (hasKey $container "args") (not (kindIs "slice" $container.args)) -}} - {{- fail "runner.container.args must be a list" -}} -{{- end -}} -{{- if and (hasKey $container "securityContext") (not (kindIs "map" $container.securityContext)) -}} - {{- fail "runner.container.securityContext must be a map/object" -}} -{{- end -}} +{{- $container := ($runner.container | default dict) -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} {{- $hookPath := (index $kubeMode "hookPath" | default "/home/runner/k8s/index.js") -}} {{- $extensionRef := (index $kubeMode "extensionRef" | default "") -}}