From 824e3b619b7da9843fd8972570207e7ff7885785 Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Wed, 9 Sep 2026 09:41:19 +0200 Subject: [PATCH] Fail with the values path when metadata is not a mapping The validation helpers ranged over labels/annotations without checking they were maps, and silently skipped non-map resourceMeta entries. A mis-typed value therefore escaped validation and failed later with an opaque error such as 'range can't iterate over oops' or 'can't evaluate field labels in type interface {}', which is the class of error this validation exists to replace. Guard every metadata map with an explicit type check that names the values path, and run the validation from every template that renders metadata so the reported error does not depend on which template Helm renders first. Also add coverage for scalar coercion driven by a values file. SetValues only ever produces strings, so the existing tests could not exercise the bool/number values that stringMap is there to handle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../templates/_helpers.tpl | 38 +++++-- .../templates/githubsecret.yaml | 1 + .../templates/hook_extension.yaml | 1 + .../templates/kube_mode_role.yaml | 1 + .../templates/kube_mode_role_binding.yaml | 1 + .../templates/kube_mode_serviceaccount.yaml | 1 + .../templates/manager_role.yaml | 1 + .../templates/manager_role_binding.yaml | 1 + .../no_permission_serviceaccount.yaml | 1 + ...g_runner_set_metadata_validation_test.yaml | 51 +++++++++ .../templates/_helpers.tpl | 32 ++++-- .../templates/githubsecret.yaml | 1 + .../templates/kube_mode_role.yaml | 1 + .../templates/kube_mode_role_binding.yaml | 1 + .../templates/kube_mode_serviceaccount.yaml | 1 + .../templates/manager_role.yaml | 1 + .../templates/manager_role_binding.yaml | 1 + .../no_permission_serviceaccount.yaml | 1 + .../tests/template_test.go | 107 ++++++++++++++++++ .../tests/values_scalar_metadata.yaml | 36 ++++++ 20 files changed, 260 insertions(+), 19 deletions(-) create mode 100644 charts/gha-runner-scale-set/tests/values_scalar_metadata.yaml diff --git a/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl b/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl index c065c5ee..7ddf2220 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl @@ -11,6 +11,19 @@ rendered as YAML booleans or numbers. {{- toYaml $out -}} {{- end }} +{{/* +Fail unless a value is absent or a mapping. Used to turn mis-typed metadata values into an +error that names the values path, instead of an opaque "range can't iterate over" further +down the render. +Expects a dict with "value" and "path". +*/}} +{{- define "assert-map" -}} +{{- $value := .value -}} +{{- if and (not (kindIs "invalid" $value)) (not (kindIs "map" $value)) -}} +{{- fail (printf "%s: must be a mapping, got %s" .path (kindOf $value)) -}} +{{- end -}} +{{- end }} + {{/* Validate a label or annotation key against the Kubernetes qualified name rules. Expects a dict with "key", "kind" (label|annotation) and "path" (the values path used in the error message). @@ -49,9 +62,11 @@ Expects a dict with "metadata" and "path". */}} {{- define "validate-metadata" -}} {{- $path := .path -}} +{{- include "assert-map" (dict "value" .metadata "path" $path) -}} {{- $metadata := .metadata | default dict -}} -{{- if kindIs "map" $metadata -}} -{{- range $key, $value := ((index $metadata "labels") | default dict) -}} +{{- $labels := index $metadata "labels" -}} +{{- include "assert-map" (dict "value" $labels "path" (printf "%s.labels" $path)) -}} +{{- range $key, $value := ($labels | default dict) -}} {{- include "validate-metadata-key" (dict "key" $key "kind" "label" "path" (printf "%s.labels" $path)) -}} {{- $rendered := printf "%v" $value -}} {{- if gt (len $rendered) 63 -}} @@ -61,25 +76,26 @@ Expects a dict with "metadata" and "path". {{- fail (printf "%s.labels: invalid value %q for label %q: a valid label value must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" $path $rendered $key) -}} {{- end -}} {{- end -}} -{{- range $key, $value := ((index $metadata "annotations") | default dict) -}} +{{- $annotations := index $metadata "annotations" -}} +{{- include "assert-map" (dict "value" $annotations "path" (printf "%s.annotations" $path)) -}} +{{- range $key, $value := ($annotations | default dict) -}} {{- include "validate-metadata-key" (dict "key" $key "kind" "annotation" "path" (printf "%s.annotations" $path)) -}} {{- end -}} -{{- end -}} {{- end }} {{/* Validate every label and annotation map the chart can render onto resources it manages. */}} {{- define "validate-all-metadata" -}} +{{- include "assert-map" (dict "value" .Values.resource "path" ".Values.resource") -}} {{- range $resource, $config := (.Values.resource | default dict) }} -{{- if kindIs "map" $config }} -{{- include "validate-metadata" (dict "metadata" (index $config "metadata") "path" (printf ".Values.resource.%s.metadata" $resource)) -}} -{{- end }} -{{- end }} -{{- $runnerPod := (index (.Values.runner | default dict) "pod") | default dict }} -{{- if kindIs "map" $runnerPod }} -{{- include "validate-metadata" (dict "metadata" (index $runnerPod "metadata") "path" ".Values.runner.pod.metadata") -}} +{{- include "assert-map" (dict "value" $config "path" (printf ".Values.resource.%s" $resource)) -}} +{{- include "validate-metadata" (dict "metadata" (index ($config | default dict) "metadata") "path" (printf ".Values.resource.%s.metadata" $resource)) -}} {{- end }} +{{- include "assert-map" (dict "value" .Values.runner "path" ".Values.runner") -}} +{{- $runnerPod := index (.Values.runner | default dict) "pod" -}} +{{- include "assert-map" (dict "value" $runnerPod "path" ".Values.runner.pod") -}} +{{- include "validate-metadata" (dict "metadata" (index ($runnerPod | default dict) "metadata") "path" ".Values.runner.pod.metadata") -}} {{- end }} {{/* diff --git a/charts/gha-runner-scale-set-experimental/templates/githubsecret.yaml b/charts/gha-runner-scale-set-experimental/templates/githubsecret.yaml index 0630891e..884b0bcb 100644 --- a/charts/gha-runner-scale-set-experimental/templates/githubsecret.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/githubsecret.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} {{- $usesKubernetesSecrets := or (not .Values.secretResolution) (eq .Values.secretResolution.type "kubernetes") -}} {{- if and (not $usesKubernetesSecrets) (empty .Values.auth.secretName) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/hook_extension.yaml b/charts/gha-runner-scale-set-experimental/templates/hook_extension.yaml index d9b4576a..b40e24c0 100644 --- a/charts/gha-runner-scale-set-experimental/templates/hook_extension.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/hook_extension.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} {{- $runner := (.Values.runner | default dict) -}} {{- $runnerMode := (index $runner "mode" | default "") -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/kube_mode_role.yaml b/charts/gha-runner-scale-set-experimental/templates/kube_mode_role.yaml index acaef34f..168a0610 100644 --- a/charts/gha-runner-scale-set-experimental/templates/kube_mode_role.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/kube_mode_role.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} {{- $runner := (.Values.runner | default dict) -}} {{- $runnerMode := (index $runner "mode" | default "") -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/kube_mode_role_binding.yaml b/charts/gha-runner-scale-set-experimental/templates/kube_mode_role_binding.yaml index c2ecc565..50334376 100644 --- a/charts/gha-runner-scale-set-experimental/templates/kube_mode_role_binding.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/kube_mode_role_binding.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} {{- $runner := (.Values.runner | default dict) -}} {{- $runnerMode := (index $runner "mode" | default "") -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/kube_mode_serviceaccount.yaml b/charts/gha-runner-scale-set-experimental/templates/kube_mode_serviceaccount.yaml index 0e8b0b7a..1c9d65a1 100644 --- a/charts/gha-runner-scale-set-experimental/templates/kube_mode_serviceaccount.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/kube_mode_serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} {{- $runner := (.Values.runner | default dict) -}} {{- $runnerMode := (index $runner "mode" | default "") -}} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/manager_role.yaml b/charts/gha-runner-scale-set-experimental/templates/manager_role.yaml index 2990ccc4..2ec8b3e7 100644 --- a/charts/gha-runner-scale-set-experimental/templates/manager_role.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/manager_role.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: diff --git a/charts/gha-runner-scale-set-experimental/templates/manager_role_binding.yaml b/charts/gha-runner-scale-set-experimental/templates/manager_role_binding.yaml index 56094cec..9cf442ae 100644 --- a/charts/gha-runner-scale-set-experimental/templates/manager_role_binding.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/manager_role_binding.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: diff --git a/charts/gha-runner-scale-set-experimental/templates/no_permission_serviceaccount.yaml b/charts/gha-runner-scale-set-experimental/templates/no_permission_serviceaccount.yaml index 6a2d53a4..1a5f99f4 100644 --- a/charts/gha-runner-scale-set-experimental/templates/no_permission_serviceaccount.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/no_permission_serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- include "validate-all-metadata" . }} {{- $runnerMode := (.Values.runner.mode | default "") -}} {{- if ne $runnerMode "kubernetes" -}} apiVersion: v1 diff --git a/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_metadata_validation_test.yaml b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_metadata_validation_test.yaml index 47f7b237..97cfc980 100644 --- a/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_metadata_validation_test.yaml +++ b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_metadata_validation_test.yaml @@ -110,3 +110,54 @@ tests: - equal: path: spec.ephemeralRunnerMetadata.annotations["enabled"] value: "true" + + - it: should fail when a metadata block is not a mapping + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + pod: + metadata: "oops" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.runner.pod.metadata: must be a mapping, got string' + + - it: should fail when a labels block is not a mapping + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + resource: + all: + metadata: + labels: "oops" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.resource.all.metadata.labels: must be a mapping, got string' + + - it: should fail when a resource entry is not a mapping + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + resource: + ephemeralRunner: "oops" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.resource.ephemeralRunner: must be a mapping, got string' diff --git a/charts/gha-runner-scale-set/templates/_helpers.tpl b/charts/gha-runner-scale-set/templates/_helpers.tpl index dfc9fcac..3c1c395c 100644 --- a/charts/gha-runner-scale-set/templates/_helpers.tpl +++ b/charts/gha-runner-scale-set/templates/_helpers.tpl @@ -67,6 +67,19 @@ rendered as YAML booleans or numbers. {{- toYaml $out -}} {{- end }} +{{/* +Fail unless a value is absent or a mapping. Used to turn mis-typed metadata values into an +error that names the values path, instead of an opaque "range can't iterate over" further +down the render. +Expects a dict with "value" and "path". +*/}} +{{- define "gha-runner-scale-set.assertMap" -}} +{{- $value := .value -}} +{{- if and (not (kindIs "invalid" $value)) (not (kindIs "map" $value)) -}} +{{- fail (printf "%s: must be a mapping, got %s" .path (kindOf $value)) -}} +{{- end -}} +{{- end }} + {{/* Validate a label or annotation key against the Kubernetes qualified name rules. Expects a dict with "key", "kind" (label|annotation) and "path" (the values path used in the error message). @@ -104,6 +117,7 @@ Expects a dict with "labels" and "path". */}} {{- define "gha-runner-scale-set.validateLabels" -}} {{- $path := .path -}} +{{- include "gha-runner-scale-set.assertMap" (dict "value" .labels "path" $path) -}} {{- range $key, $value := (.labels | default dict) -}} {{- include "gha-runner-scale-set.validateMetadataKey" (dict "key" $key "kind" "label" "path" $path) -}} {{- $rendered := printf "%v" $value -}} @@ -122,6 +136,7 @@ Expects a dict with "annotations" and "path". */}} {{- define "gha-runner-scale-set.validateAnnotations" -}} {{- $path := .path -}} +{{- include "gha-runner-scale-set.assertMap" (dict "value" .annotations "path" $path) -}} {{- range $key, $value := (.annotations | default dict) -}} {{- include "gha-runner-scale-set.validateMetadataKey" (dict "key" $key "kind" "annotation" "path" $path) -}} {{- end -}} @@ -133,19 +148,20 @@ Validate every label and annotation map the chart can render onto resources it m {{- define "gha-runner-scale-set.validateMetadata" -}} {{- include "gha-runner-scale-set.validateLabels" (dict "labels" .Values.labels "path" ".Values.labels") -}} {{- include "gha-runner-scale-set.validateAnnotations" (dict "annotations" .Values.annotations "path" ".Values.annotations") -}} -{{- with .Values.template }} -{{- with .metadata }} -{{- include "gha-runner-scale-set.validateLabels" (dict "labels" .labels "path" ".Values.template.metadata.labels") -}} -{{- include "gha-runner-scale-set.validateAnnotations" (dict "annotations" .annotations "path" ".Values.template.metadata.annotations") -}} -{{- end }} -{{- end }} +{{- include "gha-runner-scale-set.assertMap" (dict "value" .Values.template "path" ".Values.template") -}} +{{- $templateMetadata := index (.Values.template | default dict) "metadata" -}} +{{- include "gha-runner-scale-set.assertMap" (dict "value" $templateMetadata "path" ".Values.template.metadata") -}} +{{- $templateMetadata = $templateMetadata | default dict -}} +{{- include "gha-runner-scale-set.validateLabels" (dict "labels" (index $templateMetadata "labels") "path" ".Values.template.metadata.labels") -}} +{{- include "gha-runner-scale-set.validateAnnotations" (dict "annotations" (index $templateMetadata "annotations") "path" ".Values.template.metadata.annotations") -}} +{{- include "gha-runner-scale-set.assertMap" (dict "value" .Values.resourceMeta "path" ".Values.resourceMeta") -}} {{- range $resource, $meta := (.Values.resourceMeta | default dict) }} -{{- if kindIs "map" $meta }} +{{- include "gha-runner-scale-set.assertMap" (dict "value" $meta "path" (printf ".Values.resourceMeta.%s" $resource)) -}} +{{- $meta = $meta | default dict -}} {{- include "gha-runner-scale-set.validateLabels" (dict "labels" (index $meta "labels") "path" (printf ".Values.resourceMeta.%s.labels" $resource)) -}} {{- include "gha-runner-scale-set.validateAnnotations" (dict "annotations" (index $meta "annotations") "path" (printf ".Values.resourceMeta.%s.annotations" $resource)) -}} {{- end }} {{- end }} -{{- end }} {{/* Render a ResourceMeta block for AutoscalingRunnerSet spec fields. diff --git a/charts/gha-runner-scale-set/templates/githubsecret.yaml b/charts/gha-runner-scale-set/templates/githubsecret.yaml index 170c2f33..5df122b0 100644 --- a/charts/gha-runner-scale-set/templates/githubsecret.yaml +++ b/charts/gha-runner-scale-set/templates/githubsecret.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- if not (kindIs "string" .Values.githubConfigSecret) }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.githubConfigSecret) }} apiVersion: v1 diff --git a/charts/gha-runner-scale-set/templates/kube_mode_role.yaml b/charts/gha-runner-scale-set/templates/kube_mode_role.yaml index de59c69f..5a6cb7c4 100644 --- a/charts/gha-runner-scale-set/templates/kube_mode_role.yaml +++ b/charts/gha-runner-scale-set/templates/kube_mode_role.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- $containerMode := .Values.containerMode }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.kubernetesModeRole) }} {{- if and (or (eq $containerMode.type "kubernetes") (eq $containerMode.type "kubernetes-novolume")) (not .Values.template.spec.serviceAccountName) }} diff --git a/charts/gha-runner-scale-set/templates/kube_mode_role_binding.yaml b/charts/gha-runner-scale-set/templates/kube_mode_role_binding.yaml index 4145b559..bf1c6cf0 100644 --- a/charts/gha-runner-scale-set/templates/kube_mode_role_binding.yaml +++ b/charts/gha-runner-scale-set/templates/kube_mode_role_binding.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- $containerMode := .Values.containerMode }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.kubernetesModeRoleBinding) }} {{- if and (or (eq $containerMode.type "kubernetes") (eq $containerMode.type "kubernetes-novolume")) (not .Values.template.spec.serviceAccountName) }} diff --git a/charts/gha-runner-scale-set/templates/kube_mode_serviceaccount.yaml b/charts/gha-runner-scale-set/templates/kube_mode_serviceaccount.yaml index 44d977c5..d6255d3d 100644 --- a/charts/gha-runner-scale-set/templates/kube_mode_serviceaccount.yaml +++ b/charts/gha-runner-scale-set/templates/kube_mode_serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- $containerMode := .Values.containerMode }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.kubernetesModeServiceAccount) }} {{- if and (or (eq $containerMode.type "kubernetes") (eq $containerMode.type "kubernetes-novolume")) (not .Values.template.spec.serviceAccountName) }} diff --git a/charts/gha-runner-scale-set/templates/manager_role.yaml b/charts/gha-runner-scale-set/templates/manager_role.yaml index 84c159c7..fdb41eea 100644 --- a/charts/gha-runner-scale-set/templates/manager_role.yaml +++ b/charts/gha-runner-scale-set/templates/manager_role.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.managerRole) }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role diff --git a/charts/gha-runner-scale-set/templates/manager_role_binding.yaml b/charts/gha-runner-scale-set/templates/manager_role_binding.yaml index 503ef119..c4296b32 100644 --- a/charts/gha-runner-scale-set/templates/manager_role_binding.yaml +++ b/charts/gha-runner-scale-set/templates/manager_role_binding.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.managerRoleBinding) }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/charts/gha-runner-scale-set/templates/no_permission_serviceaccount.yaml b/charts/gha-runner-scale-set/templates/no_permission_serviceaccount.yaml index c7cdb1c5..a14f3531 100644 --- a/charts/gha-runner-scale-set/templates/no_permission_serviceaccount.yaml +++ b/charts/gha-runner-scale-set/templates/no_permission_serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- include "gha-runner-scale-set.validateMetadata" . }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.noPermissionServiceAccount) }} {{- $containerMode := .Values.containerMode }} {{- if and (ne $containerMode.type "kubernetes") (ne $containerMode.type "kubernetes-novolume") (not .Values.template.spec.serviceAccountName) }} diff --git a/charts/gha-runner-scale-set/tests/template_test.go b/charts/gha-runner-scale-set/tests/template_test.go index a2bce81f..46f7470a 100644 --- a/charts/gha-runner-scale-set/tests/template_test.go +++ b/charts/gha-runner-scale-set/tests/template_test.go @@ -3256,3 +3256,110 @@ func TestTemplateRenderedAutoScalingRunnerSet_ValidMetadataIsAccepted(t *testing assert.Equal(t, "", autoscalingRunnerSet.Spec.Template.Labels["empty"]) assert.Equal(t, "any value is allowed: ✅", autoscalingRunnerSet.Spec.Template.Annotations["example.com/an"]) } + +// Kubernetes only accepts string label and annotation values. Values supplied as unquoted +// YAML scalars parse as bools/numbers, so the chart has to coerce them when rendering. +// SetValues always yields strings, so this has to come from a values file to be meaningful. +func TestTemplateRenderedAutoScalingRunnerSet_ScalarMetadataValuesAreRenderedAsStrings(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") + require.NoError(t, err) + + testValuesPath, err := filepath.Abs("../tests/values_scalar_metadata.yaml") + require.NoError(t, err) + + releaseName := "test-runners" + namespaceName := "test-" + strings.ToLower(random.UniqueID()) + + options := &helm.Options{ + Logger: logger.Discard, + ValuesFiles: []string{testValuesPath}, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + // UnmarshalK8SYaml fails outright if a value decodes as a bool or number rather than a + // string, so a successful decode is itself part of the assertion. + output := helm.RenderTemplateContext(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}) + + var autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet + helm.UnmarshalK8SYaml(t, output, &autoscalingRunnerSet) + + assert.Equal(t, "true", autoscalingRunnerSet.Labels["chart-bool"]) + assert.Equal(t, "1", autoscalingRunnerSet.Labels["chart-int"]) + assert.Equal(t, "false", autoscalingRunnerSet.Annotations["chart-bool-annotation"]) + assert.Equal(t, "1.5", autoscalingRunnerSet.Annotations["chart-float-annotation"]) + + assert.Equal(t, "true", autoscalingRunnerSet.Spec.Template.Labels["pod-bool"]) + assert.Equal(t, "42", autoscalingRunnerSet.Spec.Template.Labels["pod-int"]) + assert.Equal(t, "true", autoscalingRunnerSet.Spec.Template.Annotations["pod-bool-annotation"]) + assert.Equal(t, "7", autoscalingRunnerSet.Spec.Template.Annotations["pod-int-annotation"]) + + require.NotNil(t, autoscalingRunnerSet.Spec.EphemeralRunnerMetadata) + assert.Equal(t, "false", autoscalingRunnerSet.Spec.EphemeralRunnerMetadata.Labels["runner-bool"]) + assert.Equal(t, "3", autoscalingRunnerSet.Spec.EphemeralRunnerMetadata.Labels["runner-int"]) + assert.Equal(t, "9", autoscalingRunnerSet.Spec.EphemeralRunnerMetadata.Annotations["runner-int-annotation"]) + + output = helm.RenderTemplateContext(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/githubsecret.yaml"}) + + var githubSecret corev1.Secret + helm.UnmarshalK8SYaml(t, output, &githubSecret) + + assert.Equal(t, "5", githubSecret.Labels["secret-int"]) + assert.Equal(t, "true", githubSecret.Labels["chart-bool"]) + assert.Equal(t, "1.5", githubSecret.Annotations["chart-float-annotation"]) +} + +func TestTemplateRenderedAutoScalingRunnerSet_NonMapMetadataValidationError(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") + require.NoError(t, err) + + tt := map[string]struct { + key string + value string + expectedError string + }{ + "chart labels is not a map": { + key: "labels", + value: "oops", + expectedError: ".Values.labels: must be a mapping, got string", + }, + "runner pod labels is not a map": { + key: "template.metadata.labels", + value: "oops", + expectedError: ".Values.template.metadata.labels: must be a mapping, got string", + }, + "resourceMeta entry is not a map": { + key: "resourceMeta.githubConfigSecret", + value: "oops", + expectedError: ".Values.resourceMeta.githubConfigSecret: must be a mapping, got string", + }, + } + + for name, tc := range tt { + t.Run(name, func(t *testing.T) { + t.Parallel() + + releaseName := "test-runners" + namespaceName := "test-" + strings.ToLower(random.UniqueID()) + + options := &helm.Options{ + Logger: logger.Discard, + SetValues: map[string]string{ + "githubConfigUrl": "https://github.com/actions", + "githubConfigSecret.github_token": "gh_token12345", + "controllerServiceAccount.name": "arc", + "controllerServiceAccount.namespace": "arc-system", + tc.key: tc.value, + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + _, err := helm.RenderTemplateContextE(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}) + require.Error(t, err) + assert.ErrorContains(t, err, tc.expectedError) + }) + } +} diff --git a/charts/gha-runner-scale-set/tests/values_scalar_metadata.yaml b/charts/gha-runner-scale-set/tests/values_scalar_metadata.yaml new file mode 100644 index 00000000..c1898e78 --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_scalar_metadata.yaml @@ -0,0 +1,36 @@ +githubConfigUrl: https://github.com/actions +githubConfigSecret: + github_token: gh_token12345 +controllerServiceAccount: + name: arc + namespace: arc-system + +# Every value below is an unquoted YAML scalar, so it parses as a bool, int or float +# rather than a string. Kubernetes only accepts string label and annotation values, so +# the chart has to coerce these when rendering. +labels: + chart-bool: true + chart-int: 1 +annotations: + chart-bool-annotation: false + chart-float-annotation: 1.5 + +template: + metadata: + labels: + pod-bool: true + pod-int: 42 + annotations: + pod-bool-annotation: true + pod-int-annotation: 7 + +resourceMeta: + ephemeralRunner: + labels: + runner-bool: false + runner-int: 3 + annotations: + runner-int-annotation: 9 + githubConfigSecret: + labels: + secret-int: 5