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>
This commit is contained in:
Nikola Jokic
2026-09-08 17:21:25 +02:00
co-authored by Copilot App
parent 7c68e1d318
commit c156b81e00
14 changed files with 418 additions and 35 deletions
@@ -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 }}
@@ -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) -}}
@@ -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) }}
@@ -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"
@@ -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 }}
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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 }}
@@ -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:
@@ -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:
@@ -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:
@@ -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"])
}