From 876c0a889e5b5ccea8eac5e9338821485a169d7d Mon Sep 17 00:00:00 2001 From: Michael Adriaanse Date: Mon, 3 Aug 2026 16:07:37 +0200 Subject: [PATCH] fix(gha-runner-scale-set): keep default image/command when overriding runner container resources Helm replaces list values wholesale rather than merging list items, so setting template.spec.containers to override e.g. resources on the "runner" container silently drops the chart's default image and command -- every render path (default, dind, kubernetes, kubernetes-novolume) only re-emits keys the user explicitly supplied. The resulting pod has no image and fails admission outright. Backfill the chart's default image/command when the user's override doesn't set them, in all four runner-container render paths. Backfill is coupled: command is only defaulted alongside image, so a user-supplied custom image is never paired with an assumed entrypoint it may not expect. kubernetes-novolume's ACTIONS_RUNNER_IMAGE env var now also reflects the resolved image instead of being left blank when unset. Adds template_test.go coverage for all four modes plus a regression guard confirming a custom image without a command is left untouched. --- .../templates/_helpers.tpl | 45 +++++++++ .../tests/template_test.go | 95 +++++++++++++++++++ ...values_runner_custom_image_no_command.yaml | 14 +++ .../tests/values_runner_resources_only.yaml | 13 +++ .../values_runner_resources_only_dind.yaml | 15 +++ ...lues_runner_resources_only_kubernetes.yaml | 15 +++ ...er_resources_only_kubernetes_novolume.yaml | 15 +++ 7 files changed, 212 insertions(+) create mode 100644 charts/gha-runner-scale-set/tests/values_runner_custom_image_no_command.yaml create mode 100644 charts/gha-runner-scale-set/tests/values_runner_resources_only.yaml create mode 100644 charts/gha-runner-scale-set/tests/values_runner_resources_only_dind.yaml create mode 100644 charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes.yaml create mode 100644 charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes_novolume.yaml diff --git a/charts/gha-runner-scale-set/templates/_helpers.tpl b/charts/gha-runner-scale-set/templates/_helpers.tpl index 4ad4bfef9f..35d50588f9 100644 --- a/charts/gha-runner-scale-set/templates/_helpers.tpl +++ b/charts/gha-runner-scale-set/templates/_helpers.tpl @@ -96,6 +96,22 @@ annotations: {{- include "gha-runner-scale-set.fullname" . | replace "_" "-" }}-kube-mode {{- end }} +{{/* +Default image/command for the "runner" container, mirroring values.yaml's +template.spec.containers default. Helm replaces list values wholesale rather +than merging list items, so a user-supplied template.spec.containers entry +for "runner" (e.g. to set only resources) otherwise loses image/command +entirely -- these are used as a fallback in each mode's runner-container +render logic when the user's override doesn't set that key. +*/}} +{{- define "gha-runner-scale-set.defaultRunnerImage" -}} +ghcr.io/actions/actions-runner:latest +{{- end -}} + +{{- define "gha-runner-scale-set.defaultRunnerCommand" -}} +["/home/runner/run.sh"] +{{- end -}} + {{- define "gha-runner-scale-set.dind-init-container" -}} {{- range $i, $val := .Values.template.spec.containers }} {{- if eq $val.name "runner" }} @@ -220,6 +236,13 @@ volumeMounts: {{ $key }}: {{ $val | toYaml | nindent 2 }} {{- end }} {{- end }} + {{- $usingDefaultRunnerImage := not (hasKey $container "image") }} + {{- if $usingDefaultRunnerImage }} +image: {{ include "gha-runner-scale-set.defaultRunnerImage" . }} + {{- end }} + {{- if and $usingDefaultRunnerImage (not (hasKey $container "command")) }} +command: {{ include "gha-runner-scale-set.defaultRunnerCommand" . }} + {{- end }} {{- $setDockerHost := 1 }} {{- $setRunnerWaitDocker := 1 }} {{- $setNodeExtraCaCerts := 0 }} @@ -309,6 +332,13 @@ volumeMounts: {{ $key }}: {{ $val | toYaml | nindent 2 }} {{- end }} {{- end }} + {{- $usingDefaultRunnerImage := not (hasKey $container "image") }} + {{- if $usingDefaultRunnerImage }} +image: {{ include "gha-runner-scale-set.defaultRunnerImage" . }} + {{- end }} + {{- if and $usingDefaultRunnerImage (not (hasKey $container "command")) }} +command: {{ include "gha-runner-scale-set.defaultRunnerCommand" . }} + {{- end }} {{- $setContainerHooks := 1 }} {{- $setPodName := 1 }} {{- $setRequireJobContainer := 1 }} @@ -404,6 +434,14 @@ volumeMounts: {{ $key }}: {{ $val | toYaml | nindent 2 }} {{- end }} {{- end }} + {{- $usingDefaultRunnerImage := not $setRunnerImage }} + {{- if $usingDefaultRunnerImage }} + {{- $setRunnerImage = include "gha-runner-scale-set.defaultRunnerImage" . }} +image: {{ $setRunnerImage }} + {{- end }} + {{- if and $usingDefaultRunnerImage (not (hasKey $container "command")) }} +command: {{ include "gha-runner-scale-set.defaultRunnerCommand" . }} + {{- end }} {{- $setContainerHooks := 1 }} {{- $setPodName := 1 }} {{- $setRequireJobContainer := 1 }} @@ -502,6 +540,13 @@ volumeMounts: [] {{ $key }}: {{ $val | toYaml | nindent 4 }} {{- end }} {{- end }} + {{- $usingDefaultRunnerImage := not (hasKey $container "image") }} + {{- if $usingDefaultRunnerImage }} + image: {{ include "gha-runner-scale-set.defaultRunnerImage" . }} + {{- end }} + {{- if and $usingDefaultRunnerImage (not (hasKey $container "command")) }} + command: {{ include "gha-runner-scale-set.defaultRunnerCommand" . }} + {{- end }} {{- $setNodeExtraCaCerts := 0 }} {{- $setRunnerUpdateCaCerts := 0 }} {{- if $tlsConfig.runnerMountPath }} diff --git a/charts/gha-runner-scale-set/tests/template_test.go b/charts/gha-runner-scale-set/tests/template_test.go index e9aa4c9bcb..5a6d766e11 100644 --- a/charts/gha-runner-scale-set/tests/template_test.go +++ b/charts/gha-runner-scale-set/tests/template_test.go @@ -2099,6 +2099,101 @@ func TestTemplateRenderedAutoScalingRunnerSet_ExtraContainers(t *testing.T) { assert.Equal(t, "192.0.2.1", ars.Spec.Template.Spec.DNSConfig.Nameservers[0], "DNS Nameserver should be set") } +// Overriding template.spec.containers to set only e.g. resources on the +// "runner" container must not drop the chart's default image/command -- +// Helm replaces list values wholesale rather than merging list items, so +// without a template-side fallback the resulting container has no image, +// and the pod fails admission entirely. +func TestTemplateRenderedAutoScalingRunnerSet_RunnerResourcesOnlyKeepsDefaultImageAndCommand(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") + require.NoError(t, err) + + testCases := []struct { + name string + valuesFile string + containerIndex int + }{ + {name: "default mode", valuesFile: "values_runner_resources_only.yaml", containerIndex: 0}, + {name: "dind mode", valuesFile: "values_runner_resources_only_dind.yaml", containerIndex: 0}, + {name: "kubernetes mode", valuesFile: "values_runner_resources_only_kubernetes.yaml", containerIndex: 0}, + {name: "kubernetes-novolume mode", valuesFile: "values_runner_resources_only_kubernetes_novolume.yaml", containerIndex: 0}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + testValuesPath, err := filepath.Abs("../tests/" + tc.valuesFile) + require.NoError(t, err) + + releaseName := "test-runners" + namespaceName := "test-" + strings.ToLower(random.UniqueID()) + + options := &helm.Options{ + Logger: logger.Discard, + SetValues: map[string]string{ + "controllerServiceAccount.name": "arc", + "controllerServiceAccount.namespace": "arc-system", + }, + ValuesFiles: []string{testValuesPath}, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + output := helm.RenderTemplateContext(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}, "--debug") + + var ars v1alpha1.AutoscalingRunnerSet + helm.UnmarshalK8SYaml(t, output, &ars) + + runner := ars.Spec.Template.Spec.Containers[tc.containerIndex] + assert.Equal(t, "runner", runner.Name) + assert.Equal(t, "ghcr.io/actions/actions-runner:latest", runner.Image, "runner container should keep the chart's default image") + assert.Equal(t, []string{"/home/runner/run.sh"}, runner.Command, "runner container should keep the chart's default command") + assert.Equal(t, "50m", runner.Resources.Requests.Cpu().String(), "user-supplied resources should still be applied") + assert.Equal(t, "50Mi", runner.Resources.Requests.Memory().String(), "user-supplied resources should still be applied") + assert.Equal(t, "2Gi", runner.Resources.Limits.Memory().String(), "user-supplied resources should still be applied") + }) + } +} + +// A user-supplied custom image should never be paired with a forced default +// command -- if they haven't set command either, leave it unset rather than +// assuming their custom image expects the stock runner's entrypoint. +func TestTemplateRenderedAutoScalingRunnerSet_CustomImageWithoutCommandIsNotForced(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") + require.NoError(t, err) + + testValuesPath, err := filepath.Abs("../tests/values_runner_custom_image_no_command.yaml") + require.NoError(t, err) + + releaseName := "test-runners" + namespaceName := "test-" + strings.ToLower(random.UniqueID()) + + options := &helm.Options{ + Logger: logger.Discard, + SetValues: map[string]string{ + "controllerServiceAccount.name": "arc", + "controllerServiceAccount.namespace": "arc-system", + }, + ValuesFiles: []string{testValuesPath}, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + output := helm.RenderTemplateContext(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}, "--debug") + + var ars v1alpha1.AutoscalingRunnerSet + helm.UnmarshalK8SYaml(t, output, &ars) + + runner := ars.Spec.Template.Spec.Containers[0] + assert.Equal(t, "runner", runner.Name) + assert.Equal(t, "my-custom-runner-image:latest", runner.Image, "user-supplied custom image should be respected") + assert.Empty(t, runner.Command, "command should not be forced onto a custom image") +} + func TestTemplateRenderedAutoScalingRunnerSet_RestartPolicy(t *testing.T) { t.Parallel() diff --git a/charts/gha-runner-scale-set/tests/values_runner_custom_image_no_command.yaml b/charts/gha-runner-scale-set/tests/values_runner_custom_image_no_command.yaml new file mode 100644 index 0000000000..3d71431cfa --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_runner_custom_image_no_command.yaml @@ -0,0 +1,14 @@ +githubConfigUrl: https://github.com/actions/actions-runner-controller +githubConfigSecret: + github_token: test +template: + spec: + containers: + - name: runner + image: my-custom-runner-image:latest + resources: + limits: + memory: "2Gi" + requests: + cpu: "50m" + memory: "50Mi" diff --git a/charts/gha-runner-scale-set/tests/values_runner_resources_only.yaml b/charts/gha-runner-scale-set/tests/values_runner_resources_only.yaml new file mode 100644 index 0000000000..6d0e2fb31c --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_runner_resources_only.yaml @@ -0,0 +1,13 @@ +githubConfigUrl: https://github.com/actions/actions-runner-controller +githubConfigSecret: + github_token: test +template: + spec: + containers: + - name: runner + resources: + limits: + memory: "2Gi" + requests: + cpu: "50m" + memory: "50Mi" diff --git a/charts/gha-runner-scale-set/tests/values_runner_resources_only_dind.yaml b/charts/gha-runner-scale-set/tests/values_runner_resources_only_dind.yaml new file mode 100644 index 0000000000..6e27fefcd0 --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_runner_resources_only_dind.yaml @@ -0,0 +1,15 @@ +githubConfigUrl: https://github.com/actions/actions-runner-controller +githubConfigSecret: + github_token: test +containerMode: + type: dind +template: + spec: + containers: + - name: runner + resources: + limits: + memory: "2Gi" + requests: + cpu: "50m" + memory: "50Mi" diff --git a/charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes.yaml b/charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes.yaml new file mode 100644 index 0000000000..635210c7f7 --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes.yaml @@ -0,0 +1,15 @@ +githubConfigUrl: https://github.com/actions/actions-runner-controller +githubConfigSecret: + github_token: test +containerMode: + type: kubernetes +template: + spec: + containers: + - name: runner + resources: + limits: + memory: "2Gi" + requests: + cpu: "50m" + memory: "50Mi" diff --git a/charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes_novolume.yaml b/charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes_novolume.yaml new file mode 100644 index 0000000000..c8b937b61d --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_runner_resources_only_kubernetes_novolume.yaml @@ -0,0 +1,15 @@ +githubConfigUrl: https://github.com/actions/actions-runner-controller +githubConfigSecret: + github_token: test +containerMode: + type: kubernetes-novolume +template: + spec: + containers: + - name: runner + resources: + limits: + memory: "2Gi" + requests: + cpu: "50m" + memory: "50Mi"