Validate and coerce listener metadata, and match Kubernetes key rules

Follow-up review of the metadata validation surfaced four gaps:

Listener pod metadata took the same unvalidated, uncoerced path that the
runner pod metadata used to: an invalid label only failed once the
controller created the listener pod, which is the silent failure mode
this change set exists to remove. Both charts now validate it at render
time and route its values through the string coercion.

Values loaded from a values file arrive as float64, so "%v" rendered a
large integer such as 12345678901234 in scientific notation, silently
corrupting the annotation. Integral floats are now formatted without an
exponent, and the remaining sites that quoted metadata values directly
go through the same helper.

A map or list value was flattened into Go's own formatting, producing a
meaningless "map[a:b]" label. Such values are now rejected with the
values path that produced them.

The label key prefix check rejected dot-separated segments longer than
63 characters. Kubernetes only bounds the prefix at 253 characters in
total, so the check was stricter than the API server and would have
rejected keys that already work.

While covering the listener path, the experimental chart turned out to
render invalid YAML whenever listener.podTemplate carried both metadata
and spec: the last metadata value and the following "spec:" key ended up
on the same line. That is fixed here too, with a regression test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Nikola Jokic
2026-09-09 09:56:58 +02:00
co-authored by Copilot App
parent 824e3b619b
commit 55f1ad560b
14 changed files with 319 additions and 29 deletions
@@ -3,10 +3,18 @@ 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 "metadata-value" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- printf "%.0f" . -}}
{{- else -}}
{{- printf "%v" . -}}
{{- end -}}
{{- end }}
{{- define "string-map" -}}
{{- $out := dict -}}
{{- range $k, $v := . -}}
{{- $_ := set $out $k (printf "%v" $v) -}}
{{- $_ := set $out $k (include "metadata-value" $v) -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
@@ -24,6 +32,19 @@ Expects a dict with "value" and "path".
{{- end -}}
{{- end }}
{{/*
Fail unless a metadata value is a scalar. A map or list would otherwise be flattened by the
string coercion into Go's own formatting (`map[a:b]`), which is a syntactically valid but
meaningless label or annotation, and a null would become "<nil>".
Expects a dict with "value", "key", "kind" and "path".
*/}}
{{- define "assert-scalar" -}}
{{- $value := .value -}}
{{- if or (kindIs "map" $value) (kindIs "slice" $value) (kindIs "invalid" $value) -}}
{{- fail (printf "%s: invalid value for %s %q: must be a scalar, got %s. Quote the value if it is meant to be a string" .path .kind .key (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).
@@ -40,13 +61,11 @@ Expects a dict with "key", "kind" (label|annotation) and "path" (the values path
{{- if eq (len $parts) 2 -}}
{{- $prefix := index $parts 0 -}}
{{- $name = index $parts 1 -}}
{{- if or (eq $prefix "") (gt (len $prefix) 253) -}}
{{- if gt (len $prefix) 253 -}}
{{- 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 -}}
{{- range $_, $seg := splitList "." $prefix -}}
{{- if or (eq $seg "") (gt (len $seg) 63) (not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" $seg)) -}}
{{- fail (printf "%s: invalid %s key %q: the prefix %q must be a DNS subdomain, so each dot-separated segment must be no more than 63 characters, consist of lowercase alphanumeric characters or '-', and start and end with an alphanumeric character" $path $kind $key $prefix) -}}
{{- end -}}
{{- if not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][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, so it must consist of dot-separated segments of lowercase alphanumeric characters or '-', each starting and ending with an alphanumeric character" $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)) -}}
@@ -68,7 +87,8 @@ Expects a dict with "metadata" and "path".
{{- 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 -}}
{{- include "assert-scalar" (dict "value" $value "key" $key "kind" "label" "path" (printf "%s.labels" $path)) -}}
{{- $rendered := include "metadata-value" $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 -}}
@@ -80,6 +100,7 @@ Expects a dict with "metadata" and "path".
{{- 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)) -}}
{{- include "assert-scalar" (dict "value" $value "key" $key "kind" "annotation" "path" (printf "%s.annotations" $path)) -}}
{{- end -}}
{{- end }}
@@ -96,6 +117,13 @@ Validate every label and annotation map the chart can render onto resources it m
{{- $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") -}}
{{- $listener := .Values.listener | default dict -}}
{{- if kindIs "map" $listener -}}
{{- $listenerPod := index $listener "podTemplate" -}}
{{- if kindIs "map" ($listenerPod | default dict) -}}
{{- include "validate-metadata" (dict "metadata" (index ($listenerPod | default dict) "metadata") "path" ".Values.listener.podTemplate.metadata") -}}
{{- end -}}
{{- end -}}
{{- end }}
{{/*
@@ -162,7 +190,7 @@ non-string YAML values, which Kubernetes rejects for labels and annotations.
{{- $processed := dict -}}
{{- range $key, $value := $userLabels -}}
{{- if not (hasPrefix "actions.github.com/" $key) -}}
{{- $_ := set $processed $key (printf "%v" $value) -}}
{{- $_ := set $processed $key (include "metadata-value" $value) -}}
{{- end -}}
{{- end -}}
{{- if not (empty $processed) -}}
@@ -5,9 +5,16 @@
{{- fail ".Values.listener.podTemplate must have at least metadata or spec defined" -}}
{{- end -}}
{{- with $metadata -}}
{{- $out := omit . "labels" "annotations" -}}
{{- with .labels -}}
{{- $_ := set $out "labels" (fromYaml (include "string-map" .)) -}}
{{- end -}}
{{- with .annotations -}}
{{- $_ := set $out "annotations" (fromYaml (include "string-map" .)) -}}
{{- end -}}
metadata:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- toYaml $out | nindent 2 }}
{{ end }}
{{- with $spec -}}
spec:
{{- $containers := (index . "containers" | default (list)) -}}
@@ -38,7 +38,7 @@ tests:
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, so each dot-separated segment must be no more than 63 characters, consist of lowercase alphanumeric characters or ''-'', and start and end with an alphanumeric character'
errorMessage: '.Values.runner.pod.metadata.labels: invalid label key "Invalid.Prefix/purpose": the prefix "Invalid.Prefix" must be a DNS subdomain, so it must consist of dot-separated segments of lowercase alphanumeric characters or ''-'', each starting and ending with an alphanumeric character'
- it: should fail when a resource label value is not a valid Kubernetes label value
set:
@@ -161,3 +161,96 @@ tests:
asserts:
- failedTemplate:
errorMessage: '.Values.resource.ephemeralRunner: must be a mapping, got string'
- it: should fail when a listener pod label value is invalid
set:
scaleset.name: "test"
auth.url: "https://github.com/org"
auth.githubToken: "gh_token12345"
controllerServiceAccount.name: "arc"
controllerServiceAccount.namespace: "arc-system"
listener:
podTemplate:
metadata:
labels:
purpose: "“true”"
release:
name: "test-name"
namespace: "test-namespace"
asserts:
- failedTemplate:
errorMessage: '.Values.listener.podTemplate.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 metadata value is a mapping instead of a scalar
set:
scaleset.name: "test"
auth.url: "https://github.com/org"
auth.githubToken: "gh_token12345"
controllerServiceAccount.name: "arc"
controllerServiceAccount.namespace: "arc-system"
runner:
pod:
metadata:
annotations:
nested:
inner: "value"
release:
name: "test-name"
namespace: "test-namespace"
asserts:
- failedTemplate:
errorMessage: '.Values.runner.pod.metadata.annotations: invalid value for annotation "nested": must be a scalar, got map. Quote the value if it is meant to be a string'
- it: should accept a prefix segment longer than 63 characters, matching Kubernetes
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:
? "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com/purpose"
: "yes"
release:
name: "test-name"
namespace: "test-namespace"
asserts:
- equal:
path: spec.template.metadata.labels["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com/purpose"]
value: "yes"
# A listener pod template carrying both metadata and spec used to render "true" and the
# following "spec:" key onto the same line, producing invalid YAML.
- it: should render listener metadata and spec together, coercing scalar values to strings
set:
scaleset.name: "test"
auth.url: "https://github.com/org"
auth.githubToken: "gh_token12345"
controllerServiceAccount.name: "arc"
controllerServiceAccount.namespace: "arc-system"
listener:
podTemplate:
metadata:
labels:
listener-bool: true
annotations:
listener-int: 11
spec:
containers:
- name: listener
release:
name: "test-name"
namespace: "test-namespace"
asserts:
- equal:
path: spec.listenerTemplate.metadata.labels.listener-bool
value: "true"
- equal:
path: spec.listenerTemplate.metadata.annotations.listener-int
value: "11"
- equal:
path: spec.listenerTemplate.spec.containers[0].name
value: "listener"
@@ -54,6 +54,20 @@ 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 single label or annotation value as a string.
Values from a values file arrive as float64, so "%v" would turn large integers into
scientific notation (12345678901234 -> 1.2345678901234e+13) and silently write a value the
user never asked for. Integral floats are therefore formatted without an exponent.
*/}}
{{- define "gha-runner-scale-set.metadataValue" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- printf "%.0f" . -}}
{{- else -}}
{{- printf "%v" . -}}
{{- end -}}
{{- 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
@@ -62,7 +76,7 @@ rendered as YAML booleans or numbers.
{{- define "gha-runner-scale-set.stringMap" -}}
{{- $out := dict -}}
{{- range $k, $v := . -}}
{{- $_ := set $out $k (printf "%v" $v) -}}
{{- $_ := set $out $k (include "gha-runner-scale-set.metadataValue" $v) -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
@@ -96,13 +110,11 @@ Expects a dict with "key", "kind" (label|annotation) and "path" (the values path
{{- if eq (len $parts) 2 -}}
{{- $prefix := index $parts 0 -}}
{{- $name = index $parts 1 -}}
{{- if or (eq $prefix "") (gt (len $prefix) 253) -}}
{{- if gt (len $prefix) 253 -}}
{{- 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 -}}
{{- range $_, $seg := splitList "." $prefix -}}
{{- if or (eq $seg "") (gt (len $seg) 63) (not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" $seg)) -}}
{{- fail (printf "%s: invalid %s key %q: the prefix %q must be a DNS subdomain, so each dot-separated segment must be no more than 63 characters, consist of lowercase alphanumeric characters or '-', and start and end with an alphanumeric character" $path $kind $key $prefix) -}}
{{- end -}}
{{- if not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][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, so it must consist of dot-separated segments of lowercase alphanumeric characters or '-', each starting and ending with an alphanumeric character" $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)) -}}
@@ -110,6 +122,19 @@ Expects a dict with "key", "kind" (label|annotation) and "path" (the values path
{{- end -}}
{{- end }}
{{/*
Fail unless a metadata value is a scalar. A map or list would otherwise be flattened by the
string coercion into Go's own formatting (`map[a:b]`), which is a syntactically valid but
meaningless label or annotation, and a null would become "<nil>".
Expects a dict with "value", "key", "kind" and "path".
*/}}
{{- define "gha-runner-scale-set.assertScalar" -}}
{{- $value := .value -}}
{{- if or (kindIs "map" $value) (kindIs "slice" $value) (kindIs "invalid" $value) -}}
{{- fail (printf "%s: invalid value for %s %q: must be a scalar, got %s. Quote the value if it is meant to be a string" .path .kind .key (kindOf $value)) -}}
{{- 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.
@@ -120,7 +145,8 @@ Expects a dict with "labels" and "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 -}}
{{- include "gha-runner-scale-set.assertScalar" (dict "value" $value "key" $key "kind" "label" "path" $path) -}}
{{- $rendered := include "gha-runner-scale-set.metadataValue" $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 -}}
@@ -139,6 +165,7 @@ Expects a dict with "annotations" and "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) -}}
{{- include "gha-runner-scale-set.assertScalar" (dict "value" $value "key" $key "kind" "annotation" "path" $path) -}}
{{- end -}}
{{- end }}
@@ -154,6 +181,12 @@ Validate every label and annotation map the chart can render onto resources it m
{{- $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.listenerTemplate "path" ".Values.listenerTemplate") -}}
{{- $listenerMetadata := index (.Values.listenerTemplate | default dict) "metadata" -}}
{{- include "gha-runner-scale-set.assertMap" (dict "value" $listenerMetadata "path" ".Values.listenerTemplate.metadata") -}}
{{- $listenerMetadata = $listenerMetadata | default dict -}}
{{- include "gha-runner-scale-set.validateLabels" (dict "labels" (index $listenerMetadata "labels") "path" ".Values.listenerTemplate.metadata.labels") -}}
{{- include "gha-runner-scale-set.validateAnnotations" (dict "annotations" (index $listenerMetadata "annotations") "path" ".Values.listenerTemplate.metadata.annotations") -}}
{{- include "gha-runner-scale-set.assertMap" (dict "value" .Values.resourceMeta "path" ".Values.resourceMeta") -}}
{{- range $resource, $meta := (.Values.resourceMeta | default dict) }}
{{- include "gha-runner-scale-set.assertMap" (dict "value" $meta "path" (printf ".Values.resourceMeta.%s" $resource)) -}}
@@ -20,7 +20,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -28,7 +28,7 @@ metadata:
{{- with .Values.resourceMeta.autoscalingRunnerSet.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -39,7 +39,7 @@ metadata:
{{- with .Values.annotations }}
{{- range $k, $v := . }}
{{- if not (or (hasPrefix "actions.github.com/cleanup-" $k) (eq $k "actions.github.com/values-hash")) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -47,7 +47,7 @@ metadata:
{{- with .Values.resourceMeta.autoscalingRunnerSet.annotations }}
{{- range $k, $v := . }}
{{- if not (or (hasPrefix "actions.github.com/cleanup-" $k) (eq $k "actions.github.com/values-hash")) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -161,7 +161,23 @@ spec:
{{- with .Values.listenerTemplate }}
listenerTemplate:
{{- with .metadata }}
metadata:
{{- with .labels }}
labels:
{{- include "gha-runner-scale-set.stringMap" . | nindent 8 }}
{{- end }}
{{- with .annotations }}
annotations:
{{- include "gha-runner-scale-set.stringMap" . | nindent 8 }}
{{- end }}
{{- with omit . "labels" "annotations" }}
{{- toYaml . | nindent 6 }}
{{- end }}
{{- end }}
{{- with omit . "metadata" }}
{{- toYaml . | nindent 4}}
{{- end }}
{{- end }}
{{- with .Values.listenerMetrics }}
@@ -13,7 +13,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -15,7 +15,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -14,7 +14,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -25,7 +25,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -12,7 +12,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -12,7 +12,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -14,7 +14,7 @@ metadata:
{{- with .Values.labels }}
{{- range $k, $v := . }}
{{- if not (or (hasKey $reserved $k) (hasPrefix "actions.github.com/" $k)) }}
{{ $k }}: {{ $v | quote }}
{{ $k }}: {{ include "gha-runner-scale-set.metadataValue" $v | quote }}
{{- end }}
{{- end }}
{{- end }}
@@ -3289,12 +3289,19 @@ func TestTemplateRenderedAutoScalingRunnerSet_ScalarMetadataValuesAreRenderedAsS
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, "12345678901234", autoscalingRunnerSet.Annotations["chart-big-int-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.ListenerTemplate)
assert.Equal(t, "true", autoscalingRunnerSet.Spec.ListenerTemplate.Labels["listener-bool"])
assert.Equal(t, "11", autoscalingRunnerSet.Spec.ListenerTemplate.Annotations["listener-int-annotation"])
require.Len(t, autoscalingRunnerSet.Spec.ListenerTemplate.Spec.Containers, 1)
assert.Equal(t, "listener", autoscalingRunnerSet.Spec.ListenerTemplate.Spec.Containers[0].Name)
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"])
@@ -3336,6 +3343,11 @@ func TestTemplateRenderedAutoScalingRunnerSet_NonMapMetadataValidationError(t *t
value: "oops",
expectedError: ".Values.resourceMeta.githubConfigSecret: must be a mapping, got string",
},
"listener metadata is not a map": {
key: "listenerTemplate.metadata",
value: "oops",
expectedError: ".Values.listenerTemplate.metadata: must be a mapping, got string",
},
}
for name, tc := range tt {
@@ -3363,3 +3375,91 @@ func TestTemplateRenderedAutoScalingRunnerSet_NonMapMetadataValidationError(t *t
})
}
}
func TestTemplateRenderedAutoScalingRunnerSet_ListenerMetadataIsValidated(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",
"listenerTemplate.metadata.labels.purpose": "“true”",
"listenerTemplate.spec.containers[0].name": "listener",
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}
_, err = helm.RenderTemplateE(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"})
require.Error(t, err)
assert.Contains(t, err.Error(), `.Values.listenerTemplate.metadata.labels: invalid value "“true”" for label "purpose"`)
}
func TestTemplateRenderedAutoScalingRunnerSet_NonScalarMetadataValueValidationError(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",
},
SetJsonValues: map[string]string{
"template.metadata.annotations": `{"nested":{"inner":"value"}}`,
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}
_, err = helm.RenderTemplateE(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"})
require.Error(t, err)
assert.Contains(t, err.Error(), `.Values.template.metadata.annotations: invalid value for annotation "nested": must be a scalar, got map`)
}
// Kubernetes only bounds a label key prefix at 253 characters in total, so the chart must
// not impose the stricter per-segment 63 character limit that applies to DNS labels.
func TestTemplateRenderedAutoScalingRunnerSet_LongPrefixSegmentIsAccepted(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())
key := strings.Repeat("a", 64) + ".example.com/purpose"
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." + strings.ReplaceAll(key, ".", `\.`): "yes",
},
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, "yes", autoscalingRunnerSet.Spec.Template.Labels[key])
}
@@ -14,6 +14,9 @@ labels:
annotations:
chart-bool-annotation: false
chart-float-annotation: 1.5
# Large integers must not be rendered in scientific notation. Values loaded from a file
# arrive as float64, so a naive "%v" would produce "1.2345678901234e+13".
chart-big-int-annotation: 12345678901234
template:
metadata:
@@ -34,3 +37,13 @@ resourceMeta:
githubConfigSecret:
labels:
secret-int: 5
listenerTemplate:
metadata:
labels:
listener-bool: true
annotations:
listener-int-annotation: 11
spec:
containers:
- name: listener