Validate label and annotation metadata at chart render time (#4639)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Nikola Jokic
2026-09-10 11:12:10 +02:00
committed by GitHub
co-authored by Copilot App
parent b0e69a37bd
commit adf7d0026e
21 changed files with 655 additions and 6 deletions
@@ -27,6 +27,106 @@ 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 }}
{{/*
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).
*/}}
{{- define "validate-metadata-key" -}}
{{- $key := .key -}}
{{- $kind := .kind -}}
{{- $path := .path -}}
{{- $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 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 -}}
{{- 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)) -}}
{{- 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 -}}
{{- include "assert-map" (dict "value" .metadata "path" $path) -}}
{{- $metadata := .metadata | 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)) -}}
{{- 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 -}}
{{- 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 -}}
{{- $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)) -}}
{{- include "assert-scalar" (dict "value" $value "key" $key "kind" "annotation" "path" (printf "%s.annotations" $path)) -}}
{{- 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) }}
{{- 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 }}
{{/*
Create the labels for the GitHub auth secret.
*/}}
@@ -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) }}
@@ -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) -}}
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
{{- $runner := (.Values.runner | default dict) -}}
{{- $runnerMode := (index $runner "mode" | default "") -}}
{{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}}
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
{{- $runner := (.Values.runner | default dict) -}}
{{- $runnerMode := (index $runner "mode" | default "") -}}
{{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}}
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
{{- $runner := (.Values.runner | default dict) -}}
{{- $runnerMode := (index $runner "mode" | default "") -}}
{{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}}
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
{{- $runner := (.Values.runner | default dict) -}}
{{- $runnerMode := (index $runner "mode" | default "") -}}
{{- $kubeMode := (index $runner "kubernetesMode" | default dict) -}}
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
@@ -1,3 +1,4 @@
{{- include "validate-all-metadata" . }}
{{- $runnerMode := (.Values.runner.mode | default "") -}}
{{- if ne $runnerMode "kubernetes" -}}
apiVersion: v1
@@ -0,0 +1,204 @@
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, 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:
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"
- 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'
- 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"
@@ -83,6 +83,115 @@ 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).
*/}}
{{- define "gha-runner-scale-set.validateMetadataKey" -}}
{{- $key := .key -}}
{{- $kind := .kind -}}
{{- $path := .path -}}
{{- $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 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 -}}
{{- 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)) -}}
{{- 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 }}
{{/*
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.
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) -}}
{{- 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 -}}
{{- 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 -}}
{{- 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 }}
{{/*
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") -}}
{{- 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) }}
{{- 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 }}
{{/*
Render a ResourceMeta block for AutoscalingRunnerSet spec fields.
*/}}
@@ -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:
@@ -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
@@ -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) }}
@@ -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) }}
@@ -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) }}
@@ -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
@@ -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
@@ -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) }}
@@ -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),
@@ -3203,3 +3203,222 @@ func TestTemplateRenderedAutoScalingRunnerSet_ScalarMetadataValuesAreRenderedAsS
assert.Equal(t, "true", githubSecret.Labels["chart-bool"])
assert.Equal(t, "1.5", githubSecret.Annotations["chart-float-annotation"])
}
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"])
}
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)
})
}
}
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.RenderTemplateContextE(t, t.Context(), 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])
}