feat(valkey): one Valkey instance per application (1.0.0+up9.1.2.alpine)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 4s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 11s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Successful in 1m17s

A new chart for a single Valkey instance per application, replacing the
shared cluster-redis whose separation rests on one ACL file in one
1Password field - a construction in which every change touches six
applications and which cost a month of Paperless standstill.

Shape: StatefulSet with one replica, ClusterIP Service, no cluster mode,
no Ingress and no value to add one.

The password arrives as a finished configuration line ("requirepass
<secret>") in the 1Password field auth-conf, is mounted as a file and
included by the main configuration - never in argv, never in the
environment. The include is rendered last, so nothing in extraConfig can
override it. Because a misspelled or empty field would leave an open
server that looks perfectly healthy, an init container greps the mounted
file for the line and refuses to start the pod without it; it never
prints the file.

maxmemory is derived from the computed memory limit (maxmemoryFactor,
default 0.75) and a configured value above that limit aborts the render:
otherwise the kubelet's OOM kill decides what the eviction policy was
configured to decide. Persistence and eviction are documented as pairs -
queue (PVC + noeviction, the default) and cache (emptyDir +
allkeys-lru) - and the combination "no persistence + noeviction" carries
a warning in values.yaml and in NOTES.txt.

Probes: valkey-cli ping as an exec probe, authenticated through
VALKEYCLI_AUTH read out of the mounted file, with "-e" and an assertion
on the answer PONG. Without -e valkey-cli exits 0 on an error reply
(valkey 9.1.2, src/valkey-cli.c: the config.set_errcode branch in
cliReadReply), which would make the probe unable to fail.

Hardening: uid 999 / gid 1000 - the image's own valkey user - with
fsGroup matching, runAsNonRoot, readOnlyRootFilesystem, all capabilities
dropped and seccompProfile RuntimeDefault in the pod context so the init
container is covered too. /data is the only writable path.

NetworkPolicy on by default (first chart in this repo with one), with a
switch and a list of additional namespaces; values.yaml names that it is
only a boundary where the CNI enforces policies.

Wiring: publish-valkey.yml, the chart-name choice list, ci/valkey
fixtures for the persistent variant, a plain docker-versioning Renovate
block, README chart row.
This commit is contained in:
jklein committed 2026-09-27 11:04:13 +00:00
1 parent e0b552d93e
commit 5048ce953c
18 files changed
+1674

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
{{- $replicas := include "valkey.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.valkey.replicas) -}}
{{- $limitBytes := include "valkey.memoryLimitBytes" .Values.valkey.resources -}}
{{- $maxmemory := include "valkey.maxmemoryBytes" (dict "resources" .Values.valkey.resources "maxmemory" .Values.valkey.maxmemory "maxmemoryFactor" .Values.valkey.maxmemoryFactor) -}}
Valkey {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
ACCESS — IN-CLUSTER ONLY
{{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }}.svc.cluster.local:6379
There is no Ingress and no value to add one: this is one application's cache
or queue backend, and its only authentication is the password. Applications
address it with a redis:// URL (Valkey speaks the same protocol, and every
client library still calls the scheme redis://):
redis://:<password>@{{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }}.svc.cluster.local:6379/0
The password belongs in the CONSUMING application's own vault item, where its
connection URL lives. Nothing here writes it there.
Check by hand from inside the namespace, with the password out of the pod's own
mounted file so it never reaches your shell history:
kubectl -n {{ .Release.Namespace }} exec {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 -- /bin/sh /etc/valkey/ping.sh && echo OK
THE 1PASSWORD ITEM MUST CARRY ONE FIELD, AND ITS NAME IS PART OF THE CHART
secrets.authSecretRef points at
{{ .Values.secrets.authSecretRef }}
The 1Password operator materializes that item as the Kubernetes Secret
{{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per field of the item. This chart
reads exactly one field:
auth-conf a COMPLETE Valkey configuration line, not the bare
password:
requirepass <the password>
A differently named field produces no such key, and the pod then stays in
ContainerCreating with "couldn't find key auth-conf" in its events.
Why a whole configuration line instead of the password: the file is mounted and
"include"d by the configuration, so the password never appears in the process
list, in an environment variable, or in anything Helm renders. The cost is that
a typo in that line would be a Valkey WITHOUT a password — running, healthy,
and reachable from the whole pod network, because the official image compiles
protected-mode off. An init container therefore checks the mounted file for a
"requirepass " line before the server starts and refuses to let the pod run
otherwise. If the pod is stuck in Init:Error or Init:CrashLoopBackOff, read it:
kubectl -n {{ .Release.Namespace }} logs {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 -c {{ .Release.Name }}-{{ .Chart.Name }}-check-auth
It never prints the field's contents.
Watch the materialization with
kubectl -n {{ .Release.Namespace }} get onepassworditem {{ .Release.Name }}-{{ .Chart.Name }}-secret
kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-secret
THIS RELEASE IS CONFIGURED AS A {{ if .Values.persistence.enabled }}{{ if eq .Values.valkey.maxmemoryPolicy "noeviction" }}QUEUE{{ else }}PERSISTENT CACHE{{ end }}{{ else }}{{ if eq .Values.valkey.maxmemoryPolicy "noeviction" }}VOLATILE STORE — PROBABLY BY MISTAKE{{ else }}CACHE{{ end }}{{ end }}
persistence.enabled: {{ .Values.persistence.enabled }}
valkey.maxmemoryPolicy: {{ .Values.valkey.maxmemoryPolicy }}
memory limit (computed): {{ include "valkey.humanBytes" $limitBytes }} ({{ $limitBytes }} bytes)
maxmemory: {{ include "valkey.humanBytes" $maxmemory }} ({{ $maxmemory }} bytes){{ if kindIs "invalid" .Values.valkey.maxmemory }}, derived as maxmemoryFactor {{ .Values.valkey.maxmemoryFactor | default 0.75 }} x the limit{{ else }}, set explicitly in values and checked against the limit{{ end }}
{{ if .Values.persistence.enabled }}
{{- if eq .Values.valkey.maxmemoryPolicy "noeviction" }}
The combination this chart is built for. When memory runs out Valkey REFUSES
WRITES instead of discarding keys, so a full queue is an error in the
application rather than jobs that quietly disappeared.
{{- else }}
Persistence with an evicting policy: the data survives a restart but not
memory pressure. Fine for a cache that is expensive to rebuild — wrong for a
Sidekiq or Celery queue, which would lose jobs while staying green.
{{- end }}
{{- else }}
{{- if eq .Values.valkey.maxmemoryPolicy "noeviction" }}
READ THIS: no persistence AND noeviction is almost never intended. The data is
gone on every pod restart, so nothing is being protected — but once memory is
full, writes start FAILING instead of cold keys being dropped. For a cache set
valkey.maxmemoryPolicy: allkeys-lru; for a queue set persistence.enabled: true.
{{- else }}
Cache shape: the dataset lives in the pod only. Both AOF and the RDB save
points are switched off, so a restart starts from empty and nothing is written
to ephemeral storage.
{{- end }}
{{- end }}
{{- if .Values.persistence.enabled }}
STORAGE
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.persistence.size }}, {{ if .Values.persistence.storageClass }}storage class "{{ .Values.persistence.storageClass }}"{{ else }}on the cluster's default storage class
(persistence.storageClass is unset — the default class is not encrypted){{ end }}.
Persistence mode: {{ if .Values.persistence.appendonly }}append-only file (every write appended; a crash loses at most
the last fsync interval){{ else }}RDB snapshots only (a crash loses every write since the last
snapshot, silently — the instance comes back healthy, only shorter){{ end }}.
helm uninstall does NOT delete this PVC, which is deliberate — the data
survives a reinstall of the same release name. It also means a release removed
and recreated under a DIFFERENT name starts on an empty volume while the old
one keeps occupying space. On encrypted Longhorn volumes that space is never
returned to the pool.
{{- end }}
{{ if eq $replicas "0" }}
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}valkey.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so nothing answers on
the address above and every application configured against it will fail its own
checks. Nothing else is removed: Service and Secret stay{{ if .Values.persistence.enabled }}, the PVC keeps
the data{{ end }}{{ if .Values.networkPolicy.enabled }}, and the NetworkPolicy stays in force{{ end }}.
{{ end }}
CHANGING THE PASSWORD IS NOT COVERED BY A CHECKSUM
The pod template carries checksum/config over the configuration ConfigMap and
checksum/secret-ref over the rendered OnePasswordItem. So a changed
maxmemory/policy rolls the pod, and so does repointing secrets.authSecretRef at
a DIFFERENT vault item. What cannot be covered is a new password inside the
same item: Helm never sees the content of a Secret the 1Password operator
fills in. After changing it there:
kubectl -n {{ .Release.Namespace }} rollout restart statefulset {{ .Release.Name }}-{{ .Chart.Name }}-sfs
(operator.1password.io/auto-restart is not set on purpose — the operator
restarts Deployments only, so on a StatefulSet it would be a silent no-op.)
Unverified, and named so rather than promised: because the probes read the
password out of the same mounted file, the kubelet's refresh of that file
(roughly a minute) should make the liveness probe fail against the still-old
server and restart the container by itself. Do the rollout restart instead of
waiting for it.
{{ if .Values.networkPolicy.enabled }}
NETWORK POLICY IS ACTIVE
Ingress to port 6379 is allowed from namespace {{ .Release.Namespace }}{{ if .Values.networkPolicy.allowedNamespaces }} and from
{{ join ", " .Values.networkPolicy.allowedNamespaces }}{{ end }}. Egress is unrestricted (this instance opens no
outbound connection of its own).
It is only a boundary where the CNI enforces NetworkPolicies. Where it does
not, the object is accepted and nothing is filtered — so verify it instead of
reading it off the manifest:
kubectl -n <some-other-namespace> run t --rm -it --image=docker.io/valkey/valkey:{{ .Chart.AppVersion }} \
-- valkey-cli -h {{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }} ping
That must hang or be refused. "NOAUTH Authentication required." means the
policy is not being enforced and the password is your only boundary.
{{- else }}
NO NETWORK POLICY
networkPolicy.enabled is false, so the password is the only thing between this
instance and every pod in the cluster. The official image compiles
protected-mode off, so there is no second line of defence behind it.
{{- end }}
WHAT A GREEN POD PROVES
All three probes run "valkey-cli -e ping" AUTHENTICATED with the password from
the mounted file and require the answer PONG. So Ready means: the server
accepted a connection, accepted that password, and answered.
Two things it does not mean. It does not say anything about the dataset — a
restarted instance whose AOF was lost answers PONG on an empty database. And
it does not say your application's keys are reachable: that depends on the
connection URL in the application's own vault item, which this chart neither
writes nor reads.
The probe asserts the answer (PONG), not just that the command returned, and
it passes "-e" so that valkey-cli reports an error reply through its exit code
at all — without it a bare "valkey-cli ping" exits 0 while printing
"NOAUTH Authentication required.", which would be a probe that cannot fail.
+434
View File
@@ -0,0 +1,434 @@
{{/*
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 duplicated into every chart's templates/_helpers.tpl with
the chart's name as define prefix, because sharing it would mean a library
chart, and a dependency is admissible but expensive (README, guideline 14) -
this chart takes none. template-chart holds the reference implementation, so a
fix belongs there as well and not here alone.
*/}}
{{- define "valkey.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 "valkey.resourceKey" $key) $value -}}
{{- end -}}
{{- end -}}
{{- $limits := dict -}}
{{- range $key, $value := ($resources.limits | default dict) -}}
{{- if not (kindIs "invalid" $value) -}}
{{- $_ := set $limits (include "valkey.resourceKey" $key) $value -}}
{{- end -}}
{{- end -}}
{{- range $key, $request := $requests -}}
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
{{- $_ := set $limits $key (include "valkey.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 "valkey.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 "valkey.multiplyQuantity" -}}
{{- $quantity := include "valkey.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 "valkey.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 "valkey.formatNumber" -}}
{{- if kindIs "float64" . -}}
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
{{- else -}}
{{- printf "%v" . -}}
{{- end -}}
{{- end -}}
{{/*
Render the replica count for a Deployment/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 "valkey.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".
NOTE: like the helpers above, this is duplicated into every chart's
templates/_helpers.tpl with the chart's name as define prefix, because sharing
it would mean a library chart and this chart takes no dependency (README,
guideline 14).
*/}}
{{- define "valkey.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 "valkey.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.valkey.securityContext" can itself be erased, and indexing into nil
inside a template is not an error but silently yields nothing.
*/}}
{{- define "valkey.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.
Two rules these fallbacks exist to honour (README, "Chart style guidelines"):
- Reproduce THIS chart's defaults, not the repo standard. The uid here is 999
and the gid 1000, because that is the "valkey" user of the official image
(9.1/alpine/Dockerfile: "addgroup -S -g 1000 valkey; adduser -S -G valkey
-u 999 valkey"). Restoring the repo's usual 20000/1000 would leave the
process unable to write its own data directory, and fsGroup has to keep
matching runAsGroup or the PVC is not writable at all.
- Carry the probe's CONTEXT, not just its timings. All three probes here are
exec probes, so the command IS the probe - a fallback that lost it would
leave the workload with no working check at all.
*/}}
{{- define "valkey.podSecurityContext" -}}
{{- $given := (fromYaml (include "valkey.subBlock" (dict "block" . "key" "pod" "path" "valkey.securityContext"))).value -}}
{{- $defaults := dict
"runAsNonRoot" true
"runAsUser" 999
"runAsGroup" 1000
"fsGroup" 1000
"fsGroupChangePolicy" "Always"
"seccompProfile" (dict "type" "RuntimeDefault") -}}
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" $given "path" "valkey.securityContext.pod") -}}
{{- end -}}
{{- define "valkey.containerSecurityContext" -}}
{{- $given := (fromYaml (include "valkey.subBlock" (dict "block" . "key" "container" "path" "valkey.securityContext"))).value -}}
{{- $defaults := dict
"allowPrivilegeEscalation" false
"readOnlyRootFilesystem" true
"runAsNonRoot" true
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" $given "path" "valkey.securityContext.container") -}}
{{- end -}}
{{/*
The three probes. All of them run the same script from the chart's ConfigMap,
which authenticates with the password from the mounted auth file and asserts the
answer PONG - see templates/valkey.configmap.yaml for why "-e" and
VALKEYCLI_AUTH are what turn this from a formality into a check.
*/}}
{{- define "valkey.probeCommand" -}}
{{- list "/bin/sh" "/etc/valkey/ping.sh" | toJson -}}
{{- end -}}
{{- define "valkey.livenessProbe" -}}
{{- $defaults := dict
"exec" (dict "command" (include "valkey.probeCommand" . | fromJsonArray))
"periodSeconds" 10
"timeoutSeconds" 5
"failureThreshold" 3 -}}
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.livenessProbe") -}}
{{- end -}}
{{- define "valkey.readinessProbe" -}}
{{- $defaults := dict
"exec" (dict "command" (include "valkey.probeCommand" . | fromJsonArray))
"periodSeconds" 10
"timeoutSeconds" 5
"failureThreshold" 3 -}}
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.readinessProbe") -}}
{{- end -}}
{{- define "valkey.startupProbe" -}}
{{- $defaults := dict
"exec" (dict "command" (include "valkey.probeCommand" . | fromJsonArray))
"periodSeconds" 5
"timeoutSeconds" 5
"failureThreshold" 30 -}}
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.startupProbe") -}}
{{- end -}}
{{/*
The merged resources block: the chart's defaults with the user's overrides on
top, null-safe. Everything that needs the effective numbers goes through this,
so the memory limit the configuration derives maxmemory from is the SAME number
the container is given - not a second calculation that can drift from it.
*/}}
{{- define "valkey.mergedResources" -}}
{{- $defaults := dict
"limitFactor" 3
"requests" (dict "cpu" 0.1 "memory" "256Mi" "ephemeral-storage" "64Mi") -}}
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.resources") -}}
{{- end -}}
{{- define "valkey.effectiveResources" -}}
{{- include "valkey.resources" (fromYaml (include "valkey.mergedResources" .)) -}}
{{- end -}}
{{/*
Convert a Kubernetes memory quantity to a plain byte count.
Input: a quantity (string or number). Output: the byte count as an integer
string. Needed because Valkey's maxmemory is compared against, and derived
from, the container's memory limit - and "576Mi" cannot be compared with
"1Gi" as text.
The "m" (milli) suffix is rejected rather than converted: a milli-byte memory
quantity is legal Kubernetes and meaningless here, and silently rounding it to
zero would render "maxmemory 0", which in Valkey means UNLIMITED - the exact
opposite of what this chart computes a number for.
*/}}
{{- define "valkey.quantityBytes" -}}
{{- $quantity := include "valkey.formatNumber" . -}}
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
{{- if not $number -}}
{{- fail (printf "cannot parse memory quantity %q" $quantity) -}}
{{- end -}}
{{- $suffix := trimPrefix $number $quantity -}}
{{- $factors := dict
"" 1.0
"k" 1000.0 "M" 1000000.0 "G" 1000000000.0 "T" 1000000000000.0
"Ki" 1024.0 "Mi" 1048576.0 "Gi" 1073741824.0 "Ti" 1099511627776.0 -}}
{{- if not (hasKey $factors $suffix) -}}
{{- fail (printf "unsupported memory quantity suffix %q in %q (use one of k, M, G, T, Ki, Mi, Gi, Ti, or plain bytes)" $suffix $quantity) -}}
{{- end -}}
{{- printf "%d" (int64 (mulf (float64 $number) (index $factors $suffix))) -}}
{{- end -}}
{{/*
The container's effective memory limit, as a byte count.
It is read back out of the RENDERED resources block on purpose: an explicitly
set limits.memory wins there, and a missing one is limitFactor x the request -
so this cannot disagree with what the container actually gets.
*/}}
{{- define "valkey.memoryLimitBytes" -}}
{{- $block := fromYaml (include "valkey.effectiveResources" .) -}}
{{- $limit := "" -}}
{{- if $block.limits -}}
{{- $limit = index $block.limits "memory" -}}
{{- end -}}
{{- if kindIs "invalid" $limit -}}
{{- fail "valkey: no memory limit could be determined. valkey.resources needs either requests.memory (the limit is then limitFactor x the request) or an explicit limits.memory - maxmemory is derived from it, and an unbounded instance would be OOM-killed instead of evicting." -}}
{{- end -}}
{{- include "valkey.quantityBytes" $limit -}}
{{- end -}}
{{/*
Valkey's maxmemory for this release, as a byte count.
Input: the .Values.valkey block.
- valkey.maxmemory set -> used as given, AFTER checking it against the
container's memory limit. A value above the limit ABORTS the render: it
would hand the decision "what happens when memory runs out" from the
eviction policy to the kubelet's OOM killer, and that arrives as a
restarted pod with no explanation. Not clamped, because a clamp would mean
the running instance quietly disagrees with the values file.
- otherwise -> maxmemoryFactor x the limit, floored. The
remaining fraction is not slack: client output buffers, the AOF buffer and
above all the copy-on-write pages of the fork() that snapshots or rewrites
the AOF live outside maxmemory.
*/}}
{{- define "valkey.maxmemoryBytes" -}}
{{- $limitBytes := int64 (include "valkey.memoryLimitBytes" .resources) -}}
{{- if and (not (kindIs "invalid" .maxmemory)) (ne (printf "%v" .maxmemory) "") -}}
{{- $wanted := int64 (include "valkey.quantityBytes" .maxmemory) -}}
{{- if le $wanted (int64 0) -}}
{{- fail (printf "valkey.maxmemory is %v, which converts to %d bytes. In Valkey, maxmemory 0 means UNLIMITED, so this would switch off the very bound the chart derives - leave valkey.maxmemory unset to use maxmemoryFactor x the memory limit instead." .maxmemory $wanted) -}}
{{- end -}}
{{- if gt $wanted $limitBytes -}}
{{- fail (printf "valkey.maxmemory is %v (%d bytes) and thus ABOVE the container's memory limit (%d bytes). Valkey would keep accepting data until the kubelet OOM-kills the pod, which is what the eviction policy exists to prevent. Lower valkey.maxmemory, or raise valkey.resources.requests.memory / limitFactor / limits.memory." .maxmemory $wanted $limitBytes) -}}
{{- end -}}
{{- printf "%d" $wanted -}}
{{- else -}}
{{- $factor := 0.75 -}}
{{- if not (kindIs "invalid" .maxmemoryFactor) -}}
{{- $factor = float64 .maxmemoryFactor -}}
{{- end -}}
{{- $derived := int64 (floor (mulf (float64 $limitBytes) $factor)) -}}
{{- if le $derived (int64 0) -}}
{{- fail (printf "valkey.maxmemoryFactor %v applied to the memory limit (%d bytes) yields %d bytes. Valkey reads maxmemory 0 as UNLIMITED, so this would silently remove the bound." .maxmemoryFactor $limitBytes $derived) -}}
{{- end -}}
{{- printf "%d" $derived -}}
{{- end -}}
{{- end -}}
{{/*
Render a byte count as a human-readable size, for NOTES.txt and comments only.
Never used where an exact value matters.
*/}}
{{- define "valkey.humanBytes" -}}
{{- $bytes := float64 . -}}
{{- if ge $bytes 1073741824.0 -}}
{{- printf "%.2fGi" (divf $bytes 1073741824.0) -}}
{{- else if ge $bytes 1048576.0 -}}
{{- printf "%.2fMi" (divf $bytes 1048576.0) -}}
{{- else if ge $bytes 1024.0 -}}
{{- printf "%.2fKi" (divf $bytes 1024.0) -}}
{{- else -}}
{{- printf "%d bytes" (int64 $bytes) -}}
{{- end -}}
{{- end -}}
{{/*
Additive environment entries for the Valkey container.
Input: dict "root" <root context> "env" <the env list>.
Entries are rendered through tpl, so a value may contain Helm templating, and
the value, secretKeyRef and configMapKeyRef forms are supported.
*/}}
{{- define "valkey.env" -}}
{{- $root := .root -}}
{{- range $value := .env }}
{{- if $value.value }}
- name: {{ $value.name | quote }}
# quote is required, not cosmetic: a value like "6379" 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 -}}
+164
View File
@@ -0,0 +1,164 @@
{{- $maxmemory := include "valkey.maxmemoryBytes" (dict "resources" .Values.valkey.resources "maxmemory" .Values.valkey.maxmemory "maxmemoryFactor" .Values.valkey.maxmemoryFactor) -}}
{{- $limitBytes := include "valkey.memoryLimitBytes" .Values.valkey.resources -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
data:
# The server's whole configuration. It is mounted as a directory at
# /etc/valkey and passed to the image as an argument, so the entrypoint starts
# "valkey-server /etc/valkey/valkey.conf".
#
# The pod template carries checksum/config over this ConfigMap: without it a
# changed configuration would restart nothing, and Valkey reads its
# configuration exactly once, at start.
valkey.conf: |
# Rendered by the valkey Helm chart - do not edit in the cluster, the next
# helm upgrade overwrites it. Every value below comes from values.yaml.
port 6379
# The data directory. It is also the image's WORKDIR, so this is only made
# explicit for whoever reads the file; it is a PVC when
# persistence.enabled is true and an emptyDir otherwise.
dir /data
# maxmemory is derived from the container's memory limit
# ({{ $limitBytes }} bytes = {{ include "valkey.humanBytes" $limitBytes }}), not configured independently of it:
# {{ if kindIs "invalid" .Values.valkey.maxmemory }}maxmemoryFactor {{ .Values.valkey.maxmemoryFactor | default 0.75 }} x that limit{{ else }}valkey.maxmemory was set explicitly and checked against that limit{{ end }}.
# The remainder of the limit is for what maxmemory does NOT cover: client
# output buffers, the AOF buffer, and the copy-on-write pages of the fork()
# that writes a snapshot or rewrites the AOF.
maxmemory {{ $maxmemory }}
maxmemory-policy {{ required "valkey.maxmemoryPolicy must be set (see values.yaml for the two sensible pairs)" .Values.valkey.maxmemoryPolicy }}
{{- if .Values.persistence.enabled }}
{{- if .Values.persistence.appendonly }}
# Append-only file: every write is appended, so a crash loses at most the
# last fsync interval instead of everything since the last snapshot. The
# RDB save points stay at their defaults, as a second, cheaper copy.
appendonly yes
{{- else }}
# persistence.appendonly is false: the dataset survives only as periodic RDB
# snapshots (Valkey's default save points), so a crash loses every write
# since the last one - and it does so silently, because the instance comes
# back looking healthy, only shorter.
appendonly no
{{- end }}
{{- else }}
# persistence.enabled is false: /data is an emptyDir, so nothing written
# there survives a pod restart anyway. Both persistence mechanisms are
# switched off rather than left writing into ephemeral storage - "save """
# disables the RDB save points, which are on by default.
appendonly no
save ""
{{- end }}
{{- with .Values.valkey.extraConfig }}
# valkey.extraConfig, verbatim.
{{ tpl . $ | trim | nindent 4 }}
{{- end }}
# The password, as a complete configuration line out of the 1Password item
# (field "auth-conf"), mounted as a file. Two things about this line are
# deliberate:
#
# - it is an "include" rather than a "requirepass" written here, so the
# password is never part of this ConfigMap, of the process list, or of
# anything Helm renders;
# - it is LAST. In a Valkey configuration the last occurrence of a
# directive wins, so nothing in valkey.extraConfig above can switch the
# password off, by accident or otherwise.
#
# An init container has already verified that this file carries a
# "requirepass <password>" line before the server ever starts.
include /etc/valkey-auth/auth.conf
# The probe command for all three probes. A script rather than an inline shell
# one-liner, because the two things that make it a real check need to be
# written down next to them.
ping.sh: |
#!/bin/sh
# Authenticated liveness/readiness/startup check for Valkey.
#
# WHY "-e" AND grep, i.e. two assertions rather than one: a bare
# "valkey-cli ping" probe CANNOT FAIL. Read off the source (valkey 9.1.2,
# src/valkey-cli.c): cliReadReply turns an error reply into exit(1) in
# exactly one branch, guarded by config.set_errcode, and "-e" is the only
# thing that sets it ("-e Return exit error code when command execution
# fails."). Without -e the command prints "NOAUTH Authentication required."
# and exits 0 - green, forever, whatever the server does.
#
# Either half alone would catch that: "grep -qx PONG" asserts the ANSWER, so
# it fails on an error reply even without -e. Both are here on purpose. The
# grep is the assertion; "-e" makes the failure explicit and, because the
# error then goes to stderr instead of stdout, puts the server's own message
# ("NOAUTH...", "WRONGPASS...") into the probe's failure output where
# kubectl describe shows it.
#
# WHY THE PASSWORD COMES THROUGH THE ENVIRONMENT: "valkey-cli -a <password>"
# puts the password in the process list, where every container in this pod
# can read it. VALKEYCLI_AUTH is valkey-cli's own environment variable for
# it (REDISCLI_AUTH is still honoured as the legacy spelling), and it is set
# only for the duration of this one command.
#
# Nothing here prints the password: it only ever lives in a shell variable,
# and valkey-cli's own error messages do not echo it.
set -eu
AUTH_FILE=/etc/valkey-auth/auth.conf
# The same file the server was configured from, so probe and server can
# never disagree about the password. sed's character class is written the
# POSIX way (no \+): this runs in the image's BusyBox shell.
pass="$(sed -n 's/^requirepass[[:space:]][[:space:]]*//p' "$AUTH_FILE" | head -n 1)"
# Valkey's configuration parser allows the value to be quoted; strip one
# surrounding pair of double quotes if present.
case "$pass" in
'"'*'"') pass="${pass#\"}"; pass="${pass%\"}" ;;
esac
VALKEYCLI_AUTH="$pass" valkey-cli -e -p 6379 ping | grep -qx PONG
# The init container's check. It validates the mounted auth file and does NOT
# assemble it - the password is a finished configuration line from the vault.
check-auth.sh: |
#!/bin/sh
# Refuse to start unless the mounted auth file really carries a
# "requirepass <password>" line.
#
# The failure this prevents: the password arrives as a ready-made
# configuration line from a 1Password field, and a misspelled key
# ("requirepas"), a stray quote or an empty field would leave Valkey running
# WITHOUT any password - no render error, no crash, a green pod, and an open
# server (the official image compiles protected-mode off). Of all the ways
# this chart can be misconfigured, that is the only one nothing else would
# report.
#
# It never prints the file: an error message containing the password would
# put it in the pod's event log and in every CI diagnostics dump.
set -eu
AUTH_FILE=/etc/valkey-auth/auth.conf
if [ ! -r "$AUTH_FILE" ]; then
echo "valkey: $AUTH_FILE is missing or unreadable." >&2
echo " The 1Password item behind secrets.authSecretRef must carry a field" >&2
echo " named 'auth-conf'. The 1Password operator turns each field of the" >&2
echo " item into one key of the Kubernetes Secret, so a differently named" >&2
echo " field produces no such file." >&2
exit 1
fi
if ! grep -q '^requirepass[[:space:]][[:space:]]*[^[:space:]]' "$AUTH_FILE"; then
echo "valkey: the mounted auth file carries no 'requirepass <password>' line." >&2
echo " Expected the field 'auth-conf' of the 1Password item to contain" >&2
echo " exactly one configuration line:" >&2
echo " requirepass <the password>" >&2
echo " Refusing to start: Valkey would come up without a password, report" >&2
echo " healthy, and accept connections from the whole pod network." >&2
echo " (The file's contents are deliberately not printed.)" >&2
exit 1
fi
@@ -0,0 +1,34 @@
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
# Ingress only. Egress is deliberately unrestricted: this instance opens no
# outbound connection of its own (no replication, no cluster bus), so an
# egress rule here would constrain nothing while making the object look
# stricter than it is.
policyTypes:
- Ingress
ingress:
# One rule with a list of sources. Note the shape: an empty "from" would
# allow EVERYTHING, so the namespaces are always spelled out - the release's
# own namespace first, then whatever networkPolicy.allowedNamespaces adds.
- from:
- namespaceSelector:
matchLabels:
# Set by Kubernetes itself on every namespace (>= 1.21), so this
# needs no labelling work on the target namespaces.
kubernetes.io/metadata.name: {{ .Release.Namespace }}
{{- range .Values.networkPolicy.allowedNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 6379
{{- end }}
+19
View File
@@ -0,0 +1,19 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
# Deliberately NOT null-safe: an erased persistence block yields an
# invalid PVC that is rejected at apply time, which is loud. A chart-side
# fallback would silently size somebody's queue volume - and on an
# existing release it could not help anyway, because a PVC cannot shrink.
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}
+11
View File
@@ -0,0 +1,11 @@
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
spec:
# The item must carry the field "auth-conf", holding the complete
# configuration line "requirepass <password>" - the 1Password operator
# materializes one Secret key per item field, so the field NAME is what the
# chart depends on. NOTES.txt prints this, and the init container refuses to
# start the pod if the line is not there.
itemPath: {{ required "You must provide an auth Secret Reference (secrets.authSecretRef) - the 1Password item holding this instance's 'requirepass <password>' line" .Values.secrets.authSecretRef }}
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
spec:
selector:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
ports:
- protocol: TCP
port: 6379
targetPort: valkey
name: valkey
# ClusterIP, and there is deliberately no value to change it: this chart
# deploys one application's cache or queue backend. A NodePort or LoadBalancer
# would put a Valkey whose only authentication is one password on a routable
# address. Applications reach it at
# <release>-valkey-service.<namespace>.svc.cluster.local:6379
type: ClusterIP
+162
View File
@@ -0,0 +1,162 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
replicas: {{ include "valkey.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.valkey.replicas) }}
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
template:
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
annotations:
# Hash of the rendered ConfigMap: a changed configuration changes the pod
# template and thus restarts the pod. Without it a changed maxmemory or
# eviction policy would apply to nothing - Valkey reads its
# configuration once, at start, and a mounted file is not re-read.
checksum/config: {{ include (print $.Template.BasePath "/valkey.configmap.yaml") . | sha256sum }}
# Hash of the rendered OnePasswordItem. This covers the SOURCE of the
# password, not its content: repointing secrets.authSecretRef at a
# different vault item rolls the pod instead of leaving the old password
# in the running server. A change INSIDE the item cannot be covered -
# Helm never sees a secret the 1Password operator materializes (see
# values.yaml).
#
# operator.1password.io/auto-restart is deliberately NOT set: the
# operator only restarts Deployments, so on a StatefulSet it is a silent
# no-op that advertises a guarantee which does not exist.
checksum/secret-ref: {{ include (print $.Template.BasePath "/valkey.secret.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext:
{{- include "valkey.podSecurityContext" .Values.valkey.securityContext | nindent 8 }}
initContainers:
# Validates the mounted auth file and nothing else. It does not compose
# the configuration line - that comes finished out of the vault - it only
# refuses to let the pod start when the line is missing, which is the one
# misconfiguration of this chart that would otherwise produce a healthy,
# password-less, network-reachable Valkey. The script, and the reason it
# never prints the file, are in templates/valkey.configmap.yaml.
#
# Same image as the server, so there is no second image to pin, no
# Renovate block to maintain for it and nothing extra to pull.
- name: {{ .Release.Name }}-{{ .Chart.Name }}-check-auth
image: "docker.io/valkey/valkey:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
# command, not args: args would be handed to the image's entrypoint,
# which starts valkey-server for anything that looks like a config
# file or an option.
command:
- /bin/sh
- /etc/valkey/check-auth.sh
securityContext:
{{- include "valkey.containerSecurityContext" .Values.valkey.securityContext | nindent 12 }}
# Fixed and deliberately small: this container runs one grep. Not
# values-configurable - there is nothing here to tune.
resources:
requests:
cpu: 10m
memory: 16Mi
ephemeral-storage: 1Mi
limits:
cpu: 100m
memory: 32Mi
ephemeral-storage: 10Mi
volumeMounts:
- name: config
mountPath: /etc/valkey
readOnly: true
- name: auth
mountPath: /etc/valkey-auth
readOnly: true
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
image: "docker.io/valkey/valkey:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
# args, NOT command: the image's entrypoint is
# "tini -- docker-entrypoint.sh", and replacing it would lose tini as
# PID 1 (zombie reaping) and the entrypoint's umask. An argument
# ending in .conf makes the entrypoint start
# "valkey-server /etc/valkey/valkey.conf", which is exactly what is
# wanted. Note which branch of that entrypoint is NOT taken: running as
# uid 999 skips its root-only "chown /data + setpriv" step, so the
# volume is made writable by fsGroup instead.
args:
- /etc/valkey/valkey.conf
# The chart sets no environment variable of its own - everything the
# server is told comes from its configuration file, which is what keeps
# the password out of the environment. So the block is rendered only
# when the operator actually adds an entry, instead of leaving an empty
# "env:" (i.e. env: null) in the manifest.
{{- with .Values.valkey.env }}
env:
{{- include "valkey.env" (dict "root" $ "env" .) | nindent 12 }}
{{- end }}
ports:
- containerPort: 6379
protocol: TCP
name: valkey
# Probes, resources and both security contexts go through the
# null-safe helpers (#99): an erased block means "chart defaults
# apply" instead of a vanished probe or an unhardened container.
# Never wrap these in an "if" - a guard hides the erased case
# completely instead of rendering a visible null.
livenessProbe:
{{- include "valkey.livenessProbe" .Values.valkey.livenessProbe | nindent 12 }}
readinessProbe:
{{- include "valkey.readinessProbe" .Values.valkey.readinessProbe | nindent 12 }}
startupProbe:
{{- include "valkey.startupProbe" .Values.valkey.startupProbe | nindent 12 }}
resources:
{{- include "valkey.effectiveResources" .Values.valkey.resources | nindent 12 }}
securityContext:
{{- include "valkey.containerSecurityContext" .Values.valkey.securityContext | nindent 12 }}
volumeMounts:
# The image's WORKDIR. Everything Valkey writes goes here (the AOF
# directory, RDB snapshots), and it is the only writable path the
# server needs - which is what lets readOnlyRootFilesystem stay true
# without a single extra emptyDir.
- name: data
mountPath: /data
- name: config
mountPath: /etc/valkey
readOnly: true
# The auth file, mounted as a DIRECTORY rather than with subPath, so
# the kubelet keeps it up to date when the 1Password operator writes
# a new password into the Secret. That does not restart the server by
# itself (see the annotations above).
- name: auth
mountPath: /etc/valkey-auth
readOnly: true
restartPolicy: Always
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
{{- else }}
# persistence.enabled is false: the dataset lives in the pod and is
# gone with it. The configuration switches both AOF and the RDB save
# points off in that case, so this emptyDir stays empty - it exists
# because the image's WORKDIR has to be writable.
emptyDir: {}
{{- end }}
- name: config
configMap:
name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
- name: auth
secret:
secretName: {{ .Release.Name }}-{{ .Chart.Name }}-secret
items:
# The 1Password item's field name on the left, the file name on the
# right. Spelled out rather than mounting the whole Secret, so a
# missing field fails the pod with a named reference instead of
# producing a directory without the file.
- key: auth-conf
path: auth.conf