From c156b81e00305601a4407ddcb3972b41618e95da Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Tue, 8 Sep 2026 13:33:59 +0200 Subject: [PATCH] Validate runner metadata labels and annotations at chart render time Invalid labels supplied through .Values.template.metadata.labels (or the resourceMeta blocks) are not rejected by the API server: they live inside the AutoscalingRunnerSet spec, whose CRD schema only enforces the value type. The invalid value is only caught when the controller creates the runner pod, at which point the EphemeralRunner is marked as failed with ReasonInvalidPodFailure and the scale set never produces runners. Validate every label and annotation map the charts render against the Kubernetes key/value rules so helm install/upgrade fails immediately with the offending values path, key and value. Also render all metadata values as strings. Scalars such as 'true' or '1' were previously emitted as YAML booleans and numbers, which Kubernetes rejects for labels and annotations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../templates/_autoscalingrunnerset.tpl | 4 +- .../templates/_helpers.tpl | 83 ++++++++++++- .../templates/autoscalingrunnserset.yaml | 1 + ...g_runner_set_metadata_validation_test.yaml | 112 +++++++++++++++++ .../templates/_helpers.tpl | 90 +++++++++++++- .../templates/autoscalingrunnerset.yaml | 5 +- .../templates/githubsecret.yaml | 6 +- .../templates/kube_mode_role.yaml | 6 +- .../templates/kube_mode_role_binding.yaml | 6 +- .../templates/kube_mode_serviceaccount.yaml | 6 +- .../templates/manager_role.yaml | 6 +- .../templates/manager_role_binding.yaml | 6 +- .../no_permission_serviceaccount.yaml | 6 +- .../tests/template_test.go | 116 +++++++++++++++++- 14 files changed, 418 insertions(+), 35 deletions(-) create mode 100644 charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_metadata_validation_test.yaml diff --git a/charts/gha-runner-scale-set-experimental/templates/_autoscalingrunnerset.tpl b/charts/gha-runner-scale-set-experimental/templates/_autoscalingrunnerset.tpl index 3a5f3a98..3796d7a7 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_autoscalingrunnerset.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_autoscalingrunnerset.tpl @@ -32,11 +32,11 @@ Render a ResourceMeta block for AutoscalingRunnerSet spec fields. {{- define "autoscaling-runner-set.spec-resource-metadata" -}} {{- with .labels }} labels: - {{- toYaml . | nindent 2 }} + {{- include "string-map" . | nindent 2 }} {{- end }} {{- with .annotations }} annotations: - {{- toYaml . | nindent 2 }} + {{- include "string-map" . | nindent 2 }} {{- end }} {{- end }} diff --git a/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl b/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl index 4a4b8c29..162b0dc6 100644 --- a/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl +++ b/charts/gha-runner-scale-set-experimental/templates/_helpers.tpl @@ -1,3 +1,80 @@ +{{/* +Render a labels or annotations map with all values coerced to strings. +Kubernetes only accepts string values, so scalars such as `true` or `1` must not be +rendered as YAML booleans or numbers. +*/}} +{{- define "string-map" -}} +{{- $out := dict -}} +{{- range $k, $v := . -}} +{{- $_ := set $out $k (printf "%v" $v) -}} +{{- end -}} +{{- toYaml $out -}} +{{- 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). +*/}} +{{- define "validate-metadata-key" -}} +{{- $key := .key -}} +{{- $parts := splitList "/" $key -}} +{{- $name := $key -}} +{{- if gt (len $parts) 2 -}} +{{- fail (printf "%s: invalid %s key %q: a qualified name must consist of an optional DNS subdomain prefix followed by a single '/'" .path .kind $key) -}} +{{- end -}} +{{- if eq (len $parts) 2 -}} +{{- $prefix := index $parts 0 -}} +{{- $name = index $parts 1 -}} +{{- if or (eq $prefix "") (gt (len $prefix) 253) (not (regexMatch "^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$" $prefix)) -}} +{{- fail (printf "%s: invalid %s key %q: the prefix %q must be a DNS subdomain of no more than 253 characters" .path .kind $key $prefix) -}} +{{- end -}} +{{- end -}} +{{- if or (eq $name "") (gt (len $name) 63) (not (regexMatch "^[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])?$" $name)) -}} +{{- fail (printf "%s: invalid %s key %q: the name part must be no more than 63 characters, consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" .path .kind $key) -}} +{{- end -}} +{{- end }} + +{{/* +Validate a metadata block (dict with optional "labels" and "annotations"). +Invalid runner pod labels are only rejected once the controller creates the runner pod, +which leaves the scale set without runners, so fail at render time instead. +Expects a dict with "metadata" and "path". +*/}} +{{- define "validate-metadata" -}} +{{- $path := .path -}} +{{- $metadata := .metadata | default dict -}} +{{- if kindIs "map" $metadata -}} +{{- range $key, $value := ((index $metadata "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 -}} +{{- fail (printf "%s.labels: invalid value %q for label %q: a label value must be no more than 63 characters" $path $rendered $key) -}} +{{- end -}} +{{- if not (regexMatch "^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" $rendered) -}} +{{- 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) -}} +{{- 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" -}} +{{- 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") -}} +{{- end }} +{{- end }} + {{/* Create the labels for the GitHub auth secret. */}} @@ -53,14 +130,16 @@ Reserved annotations are excluded from both levels. {{/* -Takes a map of user labels and removes the ones with "actions.github.com/" prefix +Takes a map of user labels and removes the ones with "actions.github.com/" prefix. +Values are rendered as strings so that scalars such as `true` or `1.0` do not become +non-string YAML values, which Kubernetes rejects for labels and annotations. */}} {{- define "apply-non-reserved-gha-labels-and-annotations" -}} {{- $userLabels := . -}} {{- $processed := dict -}} {{- range $key, $value := $userLabels -}} {{- if not (hasPrefix "actions.github.com/" $key) -}} - {{- $_ := set $processed $key $value -}} + {{- $_ := set $processed $key (printf "%v" $value) -}} {{- end -}} {{- end -}} {{- if not (empty $processed) -}} diff --git a/charts/gha-runner-scale-set-experimental/templates/autoscalingrunnserset.yaml b/charts/gha-runner-scale-set-experimental/templates/autoscalingrunnserset.yaml index d42e59f2..72c0be63 100644 --- a/charts/gha-runner-scale-set-experimental/templates/autoscalingrunnserset.yaml +++ b/charts/gha-runner-scale-set-experimental/templates/autoscalingrunnserset.yaml @@ -1,4 +1,5 @@ {{- $runner := (.Values.runner | default dict) }} +{{- include "validate-all-metadata" . }} {{- $runnerMode := (index $runner "mode" | default "") }} {{- $kubeMode := (index $runner "kubernetesMode" | default dict) }} {{- $dind := (index $runner "dind" | default dict) }} 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 new file mode 100644 index 00000000..182390bd --- /dev/null +++ b/charts/gha-runner-scale-set-experimental/tests/autoscaling_runner_set_metadata_validation_test.yaml @@ -0,0 +1,112 @@ +suite: "AutoscalingRunnerSet metadata validation" +templates: + - autoscalingrunnserset.yaml +tests: + - it: should fail when a runner pod label value is not a valid Kubernetes label value + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + pod: + metadata: + labels: + purpose: "“true”" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.runner.pod.metadata.labels: invalid value "“true”" for label "purpose": a valid label value must be an empty string or consist of alphanumeric characters, ''-'', ''_'' or ''.'', and must start and end with an alphanumeric character' + + - it: should fail when a runner pod label key is not a valid qualified name + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + runner: + pod: + metadata: + labels: + "Invalid.Prefix/purpose": "ci" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.runner.pod.metadata.labels: invalid label key "Invalid.Prefix/purpose": the prefix "Invalid.Prefix" must be a DNS subdomain of no more than 253 characters' + + - it: should fail when a resource label value is not a valid Kubernetes label value + 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: + team: "not valid" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.resource.all.metadata.labels: invalid value "not valid" for label "team": a valid label value must be an empty string or consist of alphanumeric characters, ''-'', ''_'' or ''.'', and must start and end with an alphanumeric character' + + - it: should fail when an annotation key is not a valid qualified name + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + resource: + all: + metadata: + annotations: + "invalid key": "value" + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - failedTemplate: + errorMessage: '.Values.resource.all.metadata.annotations: invalid annotation key "invalid key": the name part must be no more than 63 characters, consist of alphanumeric characters, ''-'', ''_'' or ''.'', and must start and end with an alphanumeric character' + + - it: should render scalar label and annotation values as strings + set: + scaleset.name: "test" + auth.url: "https://github.com/org" + auth.githubToken: "gh_token12345" + controllerServiceAccount.name: "arc" + controllerServiceAccount.namespace: "arc-system" + resource: + ephemeralRunner: + metadata: + labels: + sync-wave: 1 + annotations: + enabled: true + runner: + pod: + metadata: + labels: + enabled: true + release: + name: "test-name" + namespace: "test-namespace" + asserts: + - equal: + path: spec.template.metadata.labels["enabled"] + value: "true" + - equal: + path: spec.ephemeralRunnerMetadata.labels["sync-wave"] + value: "1" + - equal: + path: spec.ephemeralRunnerMetadata.annotations["enabled"] + value: "true" diff --git a/charts/gha-runner-scale-set/templates/_helpers.tpl b/charts/gha-runner-scale-set/templates/_helpers.tpl index 4ad4bfef..8c8cc148 100644 --- a/charts/gha-runner-scale-set/templates/_helpers.tpl +++ b/charts/gha-runner-scale-set/templates/_helpers.tpl @@ -54,17 +54,103 @@ app.kubernetes.io/name: {{ include "gha-runner-scale-set.scale-set-name" . }} app.kubernetes.io/instance: {{ include "gha-runner-scale-set.scale-set-name" . }} {{- end }} +{{/* +Render a labels or annotations map with all values coerced to strings. +Kubernetes only accepts string values, so scalars such as `true` or `1` must not be +rendered as YAML booleans or numbers. +*/}} +{{- define "gha-runner-scale-set.stringMap" -}} +{{- $out := dict -}} +{{- range $k, $v := . -}} +{{- $_ := set $out $k (printf "%v" $v) -}} +{{- end -}} +{{- toYaml $out -}} +{{- 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). +*/}} +{{- define "gha-runner-scale-set.validateMetadataKey" -}} +{{- $key := .key -}} +{{- $parts := splitList "/" $key -}} +{{- $name := $key -}} +{{- if gt (len $parts) 2 -}} +{{- fail (printf "%s: invalid %s key %q: a qualified name must consist of an optional DNS subdomain prefix followed by a single '/'" .path .kind $key) -}} +{{- end -}} +{{- if eq (len $parts) 2 -}} +{{- $prefix := index $parts 0 -}} +{{- $name = index $parts 1 -}} +{{- if or (eq $prefix "") (gt (len $prefix) 253) (not (regexMatch "^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$" $prefix)) -}} +{{- fail (printf "%s: invalid %s key %q: the prefix %q must be a DNS subdomain of no more than 253 characters" .path .kind $key $prefix) -}} +{{- end -}} +{{- end -}} +{{- if or (eq $name "") (gt (len $name) 63) (not (regexMatch "^[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])?$" $name)) -}} +{{- fail (printf "%s: invalid %s key %q: the name part must be no more than 63 characters, consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" .path .kind $key) -}} +{{- end -}} +{{- end }} + +{{/* +Validate a map of labels. Invalid labels are only rejected once the controller creates the +runner pod, which leaves the scale set without runners, so fail at render time instead. +Expects a dict with "labels" and "path". +*/}} +{{- define "gha-runner-scale-set.validateLabels" -}} +{{- $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 -}} +{{- if gt (len $rendered) 63 -}} +{{- fail (printf "%s: invalid value %q for label %q: a label value must be no more than 63 characters" $path $rendered $key) -}} +{{- end -}} +{{- if not (regexMatch "^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" $rendered) -}} +{{- fail (printf "%s: 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 -}} +{{- end }} + +{{/* +Validate a map of annotations. Annotation values are unconstrained, so only keys are validated. +Expects a dict with "annotations" and "path". +*/}} +{{- define "gha-runner-scale-set.validateAnnotations" -}} +{{- $path := .path -}} +{{- range $key, $value := (.annotations | default dict) -}} +{{- include "gha-runner-scale-set.validateMetadataKey" (dict "key" $key "kind" "annotation" "path" $path) -}} +{{- end -}} +{{- end }} + +{{/* +Validate every label and annotation map the chart can render onto resources it manages. +*/}} +{{- 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 }} +{{- range $resource, $meta := (.Values.resourceMeta | default dict) }} +{{- if kindIs "map" $meta }} +{{- 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. */}} {{- define "gha-runner-scale-set.resourceMetaSpec" -}} {{- with .labels }} labels: - {{- toYaml . | nindent 2 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 2 }} {{- end }} {{- with .annotations }} annotations: - {{- toYaml . | nindent 2 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 2 }} {{- end }} {{- end }} diff --git a/charts/gha-runner-scale-set/templates/autoscalingrunnerset.yaml b/charts/gha-runner-scale-set/templates/autoscalingrunnerset.yaml index b75e0d76..b5f5f34c 100644 --- a/charts/gha-runner-scale-set/templates/autoscalingrunnerset.yaml +++ b/charts/gha-runner-scale-set/templates/autoscalingrunnerset.yaml @@ -1,6 +1,7 @@ {{- $resourceMeta := default (dict) .Values.resourceMeta }} {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.autoscalingRunnerSet) }} {{- /* .Values.listenerConfig is validated by values.schema.json */ -}} +{{- include "gha-runner-scale-set.validateMetadata" . }} apiVersion: actions.github.com/v1alpha1 kind: AutoscalingRunnerSet metadata: @@ -218,11 +219,11 @@ spec: metadata: {{- with .labels }} labels: - {{- toYaml . | nindent 8 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 8 }} {{- end }} {{- with .annotations }} annotations: - {{- toYaml . | nindent 8 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/charts/gha-runner-scale-set/templates/githubsecret.yaml b/charts/gha-runner-scale-set/templates/githubsecret.yaml index b581c5c3..170c2f33 100644 --- a/charts/gha-runner-scale-set/templates/githubsecret.yaml +++ b/charts/gha-runner-scale-set/templates/githubsecret.yaml @@ -18,17 +18,17 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.githubConfigSecret.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.githubConfigSecret.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} finalizers: 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 f34d78bb..de59c69f 100644 --- a/charts/gha-runner-scale-set/templates/kube_mode_role.yaml +++ b/charts/gha-runner-scale-set/templates/kube_mode_role.yaml @@ -20,17 +20,17 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.kubernetesModeRole.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.kubernetesModeRole.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} finalizers: 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 67488db5..4145b559 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 @@ -19,18 +19,18 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.kubernetesModeRoleBinding.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.kubernetesModeRoleBinding.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} finalizers: 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 869df99e..44d977c5 100644 --- a/charts/gha-runner-scale-set/templates/kube_mode_serviceaccount.yaml +++ b/charts/gha-runner-scale-set/templates/kube_mode_serviceaccount.yaml @@ -9,11 +9,11 @@ metadata: {{- if or .Values.annotations $hasCustomResourceMeta }} annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.kubernetesModeServiceAccount.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- end }} @@ -30,7 +30,7 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.kubernetesModeServiceAccount.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} diff --git a/charts/gha-runner-scale-set/templates/manager_role.yaml b/charts/gha-runner-scale-set/templates/manager_role.yaml index bbf92799..84c159c7 100644 --- a/charts/gha-runner-scale-set/templates/manager_role.yaml +++ b/charts/gha-runner-scale-set/templates/manager_role.yaml @@ -17,18 +17,18 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.managerRole.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} app.kubernetes.io/component: manager-role annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.managerRole.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} finalizers: 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 108af61c..503ef119 100644 --- a/charts/gha-runner-scale-set/templates/manager_role_binding.yaml +++ b/charts/gha-runner-scale-set/templates/manager_role_binding.yaml @@ -17,18 +17,18 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.managerRoleBinding.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} app.kubernetes.io/component: manager-role-binding annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.managerRoleBinding.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} finalizers: 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 ca889d6a..c7cdb1c5 100644 --- a/charts/gha-runner-scale-set/templates/no_permission_serviceaccount.yaml +++ b/charts/gha-runner-scale-set/templates/no_permission_serviceaccount.yaml @@ -19,17 +19,17 @@ metadata: {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.noPermissionServiceAccount.labels }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} {{- include "gha-runner-scale-set.labels" . | nindent 4 }} annotations: {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- if $hasCustomResourceMeta }} {{- with .Values.resourceMeta.noPermissionServiceAccount.annotations }} - {{- toYaml . | nindent 4 }} + {{- include "gha-runner-scale-set.stringMap" . | nindent 4 }} {{- end }} {{- end }} finalizers: diff --git a/charts/gha-runner-scale-set/tests/template_test.go b/charts/gha-runner-scale-set/tests/template_test.go index 920bc8bf..a2bce81f 100644 --- a/charts/gha-runner-scale-set/tests/template_test.go +++ b/charts/gha-runner-scale-set/tests/template_test.go @@ -2684,7 +2684,7 @@ func TestCustomLabels(t *testing.T) { "controllerServiceAccount.name": "arc", "containerMode.type": "kubernetes", "controllerServiceAccount.namespace": "arc-system", - `labels.argocd\.argoproj\.io/sync-wave`: `"1"`, + `labels.argocd\.argoproj\.io/sync-wave`: "1", `labels.app\.kubernetes\.io/part-of`: "no-override", // this shouldn't be overwritten "resourceMeta.autoscalingRunnerSet.labels.ars-custom": "ars-custom-value", "resourceMeta.githubConfigSecret.labels.gh-custom": "gh-custom-value", @@ -2708,7 +2708,7 @@ func TestCustomLabels(t *testing.T) { output := helm.RenderTemplateContext(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/githubsecret.yaml"}) const targetLabel = "argocd.argoproj.io/sync-wave" - const wantCustomValue = `"1"` + const wantCustomValue = "1" const reservedLabel = "app.kubernetes.io/part-of" const wantReservedValue = "gha-rs" @@ -2783,7 +2783,7 @@ func TestCustomLabels(t *testing.T) { "githubConfigSecret.github_token": "gh_token12345", "controllerServiceAccount.name": "arc", "controllerServiceAccount.namespace": "arc-system", - `labels.argocd\.argoproj\.io/sync-wave`: `"1"`, + `labels.argocd\.argoproj\.io/sync-wave`: "1", "resourceMeta.noPermissionServiceAccount.labels.npsa-custom": "npsa-custom-value", }, KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), @@ -2815,7 +2815,7 @@ func TestCustomAnnotations(t *testing.T) { "containerMode.type": "kubernetes", "controllerServiceAccount.name": "arc", "controllerServiceAccount.namespace": "arc-system", - `annotations.argocd\.argoproj\.io/sync-wave`: `"1"`, + `annotations.argocd\.argoproj\.io/sync-wave`: "1", "resourceMeta.autoscalingRunnerSet.annotations.ars-custom": "ars-custom-value", "resourceMeta.githubConfigSecret.annotations.gh-custom": "gh-custom-value", "resourceMeta.kubernetesModeRole.annotations.kmr-custom": "kmr-custom-value", @@ -2836,7 +2836,7 @@ func TestCustomAnnotations(t *testing.T) { } const targetAnnotations = "argocd.argoproj.io/sync-wave" - const wantCustomValue = `"1"` + const wantCustomValue = "1" output := helm.RenderTemplateContext(t, t.Context(), options, helmChartPath, releaseName, []string{"templates/githubsecret.yaml"}) @@ -2904,7 +2904,7 @@ func TestCustomAnnotations(t *testing.T) { "githubConfigSecret.github_token": "gh_token12345", "controllerServiceAccount.name": "arc", "controllerServiceAccount.namespace": "arc-system", - `annotations.argocd\.argoproj\.io/sync-wave`: `"1"`, + `annotations.argocd\.argoproj\.io/sync-wave`: "1", "resourceMeta.noPermissionServiceAccount.annotations.npsa-custom": "npsa-custom-value", }, KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), @@ -3152,3 +3152,107 @@ func TestAutoscalingRunnerSetCustomAnnotationsAndLabelsApplied(t *testing.T) { assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Annotations["actions.github.com/cleanup-manager-role-name"]) assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Labels["app.kubernetes.io/component"]) } + +func TestTemplateRenderedAutoScalingRunnerSet_InvalidMetadataValidationError(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 + }{ + "runner pod label with smart quotes": { + key: "template.metadata.labels.custom", + value: "\u201ctrue\u201d", + expectedError: `.Values.template.metadata.labels: invalid value "“true”" for label "custom"`, + }, + "runner pod label value too long": { + key: "template.metadata.labels.custom", + value: strings.Repeat("a", 64), + expectedError: `.Values.template.metadata.labels: invalid value "` + strings.Repeat("a", 64) + `" for label "custom": a label value must be no more than 63 characters`, + }, + "runner pod label key with invalid prefix": { + key: "template.metadata.labels.Invalid\\.Prefix/custom", + value: "value", + expectedError: `.Values.template.metadata.labels: invalid label key "Invalid.Prefix/custom"`, + }, + "runner pod annotation key invalid": { + key: "template.metadata.annotations.invalid key", + value: "value", + expectedError: `.Values.template.metadata.annotations: invalid annotation key "invalid key"`, + }, + "chart level label invalid": { + key: "labels.custom", + value: "not valid", + expectedError: `.Values.labels: invalid value "not valid" for label "custom"`, + }, + "resource meta label invalid": { + key: "resourceMeta.ephemeralRunner.labels.custom", + value: "not valid", + expectedError: `.Values.resourceMeta.ephemeralRunner.labels: invalid value "not valid" for label "custom"`, + }, + } + + 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) + }) + } +} + +func TestTemplateRenderedAutoScalingRunnerSet_ValidMetadataIsAccepted(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") + require.NoError(t, err) + + 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", + "template.metadata.labels.custom": "true", + "template.metadata.labels.example\\.com/custom": "value_1.0-rc", + "template.metadata.labels.empty": "", + "template.metadata.annotations.example\\.com/an": "any value is allowed: ✅", + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + 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.Spec.Template.Labels["custom"]) + assert.Equal(t, "value_1.0-rc", autoscalingRunnerSet.Spec.Template.Labels["example.com/custom"]) + assert.Equal(t, "", autoscalingRunnerSet.Spec.Template.Labels["empty"]) + assert.Equal(t, "any value is allowed: ✅", autoscalingRunnerSet.Spec.Template.Annotations["example.com/an"]) +}