Second install-test run, three findings, all measured: - Readiness on "/" failed with "http: server gave HTTP response to HTTPS client" on a pod that was answering correctly. The kubelet's HTTP prober follows redirects to the same host, and with RAILS_ASSUME_SSL=true the app builds its redirect as https://<pod-ip>:3000/registration/new, which Puma does not speak. The probe now runs curl without -L and accepts 2xx-3xx itself. The same log also proves what the probe is for: against an empty database the redirect target was /registration/new, the branch User.count decides, so the path really does reach Postgres. - The pod was evicted within two minutes of every start: "ephemeral local storage usage exceeds the total limit of containers 60Mi". emptyDirs and the stdout log both count there, and the image rebuilds its Bootsnap cache under /rails/tmp on every start because the emptyDir hides the one baked into the image. The request is now 256Mi (limit 768Mi) per app container - headroom, not a measurement. - /home/rails is read-only, so Bundler created a throwaway home under TMPDIR on every invocation, worker probe included. HOME now points at a directory the init container creates. The CI postgres fixture gets -d sure on its readiness probe, so the server stops logging a missing database that nothing asked for.
452 lines
18 KiB
Smarty
452 lines
18 KiB
Smarty
{{/*
|
|
Render the body of a container "resources:" block from a values dict of the form
|
|
|
|
resources:
|
|
limitFactor: 3 # optional, default 3
|
|
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
|
limits: { ... } # optional, every entry optional
|
|
|
|
Rules (variant B, see issue #19):
|
|
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
|
to the Kubernetes key "ephemeral-storage").
|
|
- an explicitly set limit always wins.
|
|
- a missing memory / ephemeral-storage limit is computed as
|
|
limitFactor x the corresponding request.
|
|
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
|
unlimited; CPU is compressible, so this cannot kill the workload).
|
|
|
|
NOTE: this helper is copied into every chart's templates/_helpers.tpl with the
|
|
chart's name as define prefix rather than shared through a library chart; the
|
|
reference implementation is template-chart/templates/_helpers.tpl.
|
|
*/}}
|
|
{{- define "sure.resources" -}}
|
|
{{- $resources := . | default dict -}}
|
|
{{- $factor := 3.0 -}}
|
|
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
|
{{- $factor = float64 $resources.limitFactor -}}
|
|
{{- end -}}
|
|
{{- $requests := dict -}}
|
|
{{- range $key, $value := ($resources.requests | default dict) -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $_ := set $requests (include "sure.resourceKey" $key) $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $limits := dict -}}
|
|
{{- range $key, $value := ($resources.limits | default dict) -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $_ := set $limits (include "sure.resourceKey" $key) $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- range $key, $request := $requests -}}
|
|
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
|
{{- $_ := set $limits $key (include "sure.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $out := dict -}}
|
|
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
|
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
|
{{- toYaml $out -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Normalize a resource name from values to its Kubernetes key.
|
|
*/}}
|
|
{{- define "sure.resourceKey" -}}
|
|
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
|
Input: dict "quantity" <string|number> "factor" <float64>.
|
|
Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
|
|
("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer
|
|
results are printed without a decimal point.
|
|
*/}}
|
|
{{- define "sure.multiplyQuantity" -}}
|
|
{{- $quantity := include "sure.formatNumber" .quantity -}}
|
|
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
|
{{- if not $number -}}
|
|
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
|
{{- end -}}
|
|
{{- $suffix := trimPrefix $number $quantity -}}
|
|
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
|
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
|
{{- end -}}
|
|
{{- $product := mulf (float64 $number) .factor -}}
|
|
{{- if eq $product (floor $product) -}}
|
|
{{- printf "%d%s" (int64 $product) $suffix -}}
|
|
{{- else -}}
|
|
{{- printf "%s%s" (include "sure.formatNumber" $product) $suffix -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render a number as a plain decimal string, never in exponential notation.
|
|
|
|
Why this exists (issue #157): the null-safe fallback helpers pass values
|
|
through fromYaml, which parses every number as float64. Go's "%v" switches
|
|
float64 to exponential notation from about 1e6 upwards, so a bare byte count
|
|
like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the
|
|
render aborted. Strings ("512Mi") are passed through untouched.
|
|
*/}}
|
|
{{- define "sure.formatNumber" -}}
|
|
{{- if kindIs "float64" . -}}
|
|
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
|
{{- else -}}
|
|
{{- printf "%v" . -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render the replica count for the StatefulSet.
|
|
|
|
Input: dict
|
|
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
|
|
"replicas" the workload's replicas value (may be nil)
|
|
|
|
Rules (issue #25):
|
|
- scaleDown: true takes precedence and always yields 0.
|
|
- otherwise an explicitly set replicas value is used as given, including an
|
|
explicit 0 - Helm's "default" would swallow 0, hence the explicit
|
|
kindIs "invalid" nil check.
|
|
- a missing replicas value defaults to 1.
|
|
*/}}
|
|
{{- define "sure.replicas" -}}
|
|
{{- if .scaleDown -}}
|
|
0
|
|
{{- else if not (kindIs "invalid" .replicas) -}}
|
|
{{- .replicas -}}
|
|
{{- else -}}
|
|
1
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
|
null / empty override falls back to the defaults instead of erasing them.
|
|
|
|
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
|
|
values path used in error messages>. Output: YAML of the merged dict (consume
|
|
it with fromYaml).
|
|
|
|
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
|
|
an override key written without children - e.g. a leftover
|
|
|
|
securityContext:
|
|
|
|
after deleting its last remaining sub-key - is YAML null. Where the chart
|
|
defines a default, Helm treats that null as a deletion of the default; the
|
|
template then renders "securityContext: null": no readOnlyRootFilesystem, no
|
|
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
|
|
and reports healthy while being unhardened, so nothing points at the mistake.
|
|
The same shape erases a probe (it disappears) or the resources block (no
|
|
requests, no computed limits). This helper defines the opposite semantics: an
|
|
empty block means "chart defaults apply".
|
|
*/}}
|
|
{{- define "sure.defaultedDict" -}}
|
|
{{- $defaults := .defaults | default dict -}}
|
|
{{- $given := .given -}}
|
|
{{- $path := .path | default "value" -}}
|
|
{{- if kindIs "invalid" $given -}}
|
|
{{- $given = dict -}}
|
|
{{- end -}}
|
|
{{- if not (kindIs "map" $given) -}}
|
|
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
|
|
{{- end -}}
|
|
{{- $out := deepCopy $defaults -}}
|
|
{{- range $key, $value := $given -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $default := index $defaults $key -}}
|
|
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
|
|
{{- $_ := set $out $key (fromYaml (include "sure.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
|
|
{{- else if kindIs "map" $default -}}
|
|
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
|
|
{{- else -}}
|
|
{{- $_ := set $out $key $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- toYaml $out -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Pick one sub-block out of a values block that may itself be null.
|
|
|
|
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
|
|
Returns YAML of dict "value" <the sub-block, may be nil>, after rejecting a
|
|
non-mapping block with a message that names the values path. Needed because
|
|
".Values.sure.web.securityContext" can itself be erased, and indexing into nil
|
|
inside a template is not an error but silently yields nothing.
|
|
*/}}
|
|
{{- define "sure.subBlock" -}}
|
|
{{- $block := .block -}}
|
|
{{- if kindIs "invalid" $block -}}
|
|
{{- $block = dict -}}
|
|
{{- end -}}
|
|
{{- if not (kindIs "map" $block) -}}
|
|
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
|
{{- end -}}
|
|
{{- toYaml (dict "value" (index $block .key)) -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The effective security contexts, probes and resources (issue #99).
|
|
|
|
The defaults below MUST stay in sync with the corresponding blocks in
|
|
values.yaml, which stays the documented, user-facing place for them; this copy
|
|
is only the fallback for the case where an override has erased the block. Every
|
|
reason behind a value is written down there, including what each probe actually
|
|
proves - a probe default without its path, port name or exec command would
|
|
leave the workload with no working probe at all (README, guideline 12.5).
|
|
*/}}
|
|
{{- define "sure.podSecurityContext" -}}
|
|
{{- $given := (fromYaml (include "sure.subBlock" (dict "block" . "key" "pod" "path" "sure.securityContext"))).value -}}
|
|
{{- $defaults := dict
|
|
"runAsNonRoot" true
|
|
"runAsUser" 1000
|
|
"runAsGroup" 1000
|
|
"fsGroup" 1000
|
|
"fsGroupChangePolicy" "Always"
|
|
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" $given "path" "sure.securityContext.pod") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.containerSecurityContext" -}}
|
|
{{- $given := (fromYaml (include "sure.subBlock" (dict "block" .block "key" "container" "path" .path))).value -}}
|
|
{{- $defaults := dict
|
|
"allowPrivilegeEscalation" false
|
|
"readOnlyRootFilesystem" true
|
|
"runAsNonRoot" true
|
|
"capabilities" (dict "drop" (list "ALL")) -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" $given "path" (printf "%s.container" .path)) -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Web container probes. /up is Rails' boot check and touches neither Postgres nor
|
|
Redis, so liveness and startup use it; readiness uses "/", which runs
|
|
User.count through authenticate_user! in self-hosted mode and therefore fails
|
|
while the database is unreachable. The long startup budget covers the
|
|
"./bin/rails db:prepare" the entrypoint runs before Puma binds the port.
|
|
*/}}
|
|
{{- define "sure.webLivenessProbe" -}}
|
|
{{- $defaults := dict
|
|
"httpGet" (dict "path" "/up" "port" "http")
|
|
"periodSeconds" 10
|
|
"timeoutSeconds" 5
|
|
"failureThreshold" 3 -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.livenessProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.webReadinessCommand" -}}
|
|
{{- list "/bin/bash" "-c" "code=\"$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/)\"; test \"$code\" -ge 200 -a \"$code\" -lt 400" | toJson -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.webReadinessProbe" -}}
|
|
{{- $defaults := dict
|
|
"exec" (dict "command" (include "sure.webReadinessCommand" . | fromJsonArray))
|
|
"periodSeconds" 10
|
|
"timeoutSeconds" 10
|
|
"failureThreshold" 3 -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.readinessProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.webStartupProbe" -}}
|
|
{{- $defaults := dict
|
|
"httpGet" (dict "path" "/up" "port" "http")
|
|
"periodSeconds" 10
|
|
"timeoutSeconds" 5
|
|
"failureThreshold" 60 -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.startupProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Worker container probes. Sidekiq serves no HTTP, so all three ask its own
|
|
monitor whether THIS process still holds a heartbeat in Redis; the exec command
|
|
is part of the default, not decoration (README, guideline 12.5).
|
|
*/}}
|
|
{{- define "sure.workerProbeCommand" -}}
|
|
{{- list "/bin/bash" "-c" "bundle exec sidekiqmon processes | grep -q \"^${HOSTNAME}:\"" | toJson -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.workerLivenessProbe" -}}
|
|
{{- $defaults := dict
|
|
"exec" (dict "command" (include "sure.workerProbeCommand" . | fromJsonArray))
|
|
"periodSeconds" 60
|
|
"timeoutSeconds" 15
|
|
"failureThreshold" 5 -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.livenessProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.workerReadinessProbe" -}}
|
|
{{- $defaults := dict
|
|
"exec" (dict "command" (include "sure.workerProbeCommand" . | fromJsonArray))
|
|
"periodSeconds" 30
|
|
"timeoutSeconds" 15
|
|
"failureThreshold" 3 -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.readinessProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.workerStartupProbe" -}}
|
|
{{- $defaults := dict
|
|
"exec" (dict "command" (include "sure.workerProbeCommand" . | fromJsonArray))
|
|
"periodSeconds" 20
|
|
"timeoutSeconds" 15
|
|
"failureThreshold" 30 -}}
|
|
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.startupProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The effective resources blocks, fed into the resources helper above so that an
|
|
erased "resources:" still yields the chart's requests and computed limits
|
|
instead of a container without any resource accounting. Both containers run the
|
|
same image and carry the same request; see values.yaml for where the number
|
|
comes from.
|
|
*/}}
|
|
{{- define "sure.webResources" -}}
|
|
{{- $defaults := dict
|
|
"limitFactor" 3
|
|
"requests" (dict "cpu" 0.1 "memory" "512Mi" "ephemeral-storage" "256Mi") -}}
|
|
{{- $merged := fromYaml (include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.resources")) -}}
|
|
{{- include "sure.resources" $merged -}}
|
|
{{- end -}}
|
|
|
|
{{- define "sure.workerResources" -}}
|
|
{{- $defaults := dict
|
|
"limitFactor" 3
|
|
"requests" (dict "cpu" 0.1 "memory" "512Mi" "ephemeral-storage" "256Mi") -}}
|
|
{{- $merged := fromYaml (include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.resources")) -}}
|
|
{{- include "sure.resources" $merged -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The environment both containers share: the same image, the same database, the
|
|
same Redis, the same secrets. Only the command differs.
|
|
|
|
Input: the root context.
|
|
|
|
ingress.domain is read HERE, outside the Ingress template, three times over
|
|
(APP_DOMAIN, WEBAUTHN_RP_ID, WEBAUTHN_ALLOWED_ORIGINS), so it carries its own
|
|
required() and stays mandatory when ingress.enabled is false - README,
|
|
guideline 12.1. An empty value would not fail the render: it would register
|
|
passkeys against an empty relying party and build mail links without a host.
|
|
|
|
SELF_HOSTED=true is not optional for this chart: it is what turns Sure into the
|
|
single-family, no-subscription mode, and it is also what makes the readiness
|
|
probe on "/" reach the database at all (see values.yaml).
|
|
*/}}
|
|
{{- define "sure.commonEnv" -}}
|
|
{{- $domain := required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain -}}
|
|
- name: SELF_HOSTED
|
|
value: "true"
|
|
- name: RAILS_ENV
|
|
value: "production"
|
|
# Not cosmetic and not optional: /tmp is an emptyDir, which Kubernetes creates
|
|
# mode 0777 without a sticky bit, and Ruby skips exactly that shape when it
|
|
# looks for a temporary directory. Without this the app aborts during boot with
|
|
# "could not find a temporary directory". The directory is created by the
|
|
# init container in the StatefulSet, which explains the whole case.
|
|
- name: TMPDIR
|
|
value: "/tmp/ruby"
|
|
# The image's home directory (/home/rails) is part of the read-only root
|
|
# filesystem, and Bundler reacts to that by creating a throwaway home under
|
|
# TMPDIR on EVERY invocation ("`/home/rails` is not writable. Bundler will use
|
|
# `/tmp/ruby/bundler2026...` as your home directory temporarily", measured in
|
|
# the install-test). That is one directory per `bundle exec` - including every
|
|
# worker probe, every 30 seconds. Pointing HOME at a writable directory turns
|
|
# that into one stable path.
|
|
- name: HOME
|
|
value: "/tmp/home"
|
|
- name: PORT
|
|
value: "3000"
|
|
# Sure's production.rb defaults BOTH of these to true. The chart renders them
|
|
# explicitly from ingress.enabled instead: with the Ingress terminating TLS,
|
|
# assume_ssl makes Rails treat the forwarded request as secure (so force_ssl
|
|
# adds HSTS and secure cookies without redirecting), while without an Ingress
|
|
# both have to be off - a port-forwarded plain-HTTP session would otherwise
|
|
# either be redirected to https or hand out cookies the browser never returns.
|
|
- name: RAILS_ASSUME_SSL
|
|
value: "{{ .Values.ingress.enabled }}"
|
|
- name: RAILS_FORCE_SSL
|
|
value: "{{ .Values.ingress.enabled }}"
|
|
# Action Mailer builds absolute URLs from this host, and it is the fallback the
|
|
# WebAuthn initializer uses when WEBAUTHN_RP_ID is unset.
|
|
- name: APP_DOMAIN
|
|
value: {{ $domain | quote }}
|
|
# Passkeys are bound to the relying party id; changing it later invalidates
|
|
# every registered credential (upstream docs/hosting/webauthn.md).
|
|
- name: WEBAUTHN_RP_ID
|
|
value: {{ $domain | quote }}
|
|
- name: WEBAUTHN_ALLOWED_ORIGINS
|
|
value: {{ printf "https://%s" $domain | quote }}
|
|
- name: SECRET_KEY_BASE
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
|
key: secret-key-base
|
|
- name: DB_HOST
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
key: host
|
|
- name: DB_PORT
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
key: port
|
|
- name: POSTGRES_DB
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
key: database
|
|
- name: POSTGRES_USER
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
key: username
|
|
- name: POSTGRES_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
key: password
|
|
- name: REDIS_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret
|
|
key: connection-url
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The additive env block of one container (README, guideline 10).
|
|
|
|
Input: dict "root" <root context> "env" <the container's env list>.
|
|
Entries are rendered through tpl, so a value may contain Helm templating, and
|
|
the value, secretKeyRef and configMapKeyRef forms are supported.
|
|
*/}}
|
|
{{- define "sure.env" -}}
|
|
{{- $root := .root -}}
|
|
{{- range $value := .env }}
|
|
{{- if $value.value }}
|
|
- name: {{ $value.name | quote }}
|
|
# quote is required, not cosmetic: a value like "3" or "true" renders
|
|
# unquoted as a YAML number/bool, and the API server then rejects the whole
|
|
# manifest with "cannot unmarshal number into Go struct field EnvVar.value of
|
|
# type string".
|
|
value: {{ tpl $value.value $root | quote }}
|
|
{{- else if $value.valueFrom }}
|
|
{{- if $value.valueFrom.secretKeyRef }}
|
|
- name: {{ $value.name }}
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ tpl $value.valueFrom.secretKeyRef.name $root | quote }}
|
|
key: {{ tpl $value.valueFrom.secretKeyRef.key $root | quote }}
|
|
{{- else if $value.valueFrom.configMapKeyRef }}
|
|
- name: {{ $value.name }}
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: {{ tpl $value.valueFrom.configMapKeyRef.name $root | quote }}
|
|
key: {{ tpl $value.valueFrom.configMapKeyRef.key $root | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end -}}
|