mirror of
https://github.com/actions-runner-controller/actions-runner-controller.git
synced 2026-09-30 02:35:57 +02:00
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:
co-authored by
Copilot App
parent
b0e69a37bd
commit
adf7d0026e
@@ -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
|
||||
|
||||
+204
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user