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