feat(paperless-ngx): optional valkey subchart, password from the redis item
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 12s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 6s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 22s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m31s
Validate Helm Charts / Install-test (pull_request) Successful in 3m31s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 33s

paperless-ngx cannot run without a Redis - PAPERLESS_REDIS is the Celery
broker - so every installation needed a second Helm release whose address
lived in a 1Password item and in neither chart. This adds our own published
valkey chart as an optional dependency, off by default and gated on
valkeySubchart.enabled (not valkey.enabled, which the subchart's strict
schema rejects and which Helm would only have complained about for whoever
first switched it on). Additive, default behaviour unchanged: minor,
12.1.3+up3.2.1 -> 12.2.0+up3.2.1.

Configured as a QUEUE (PVC + noeviction, the subchart's defaults), unlike
stalwart's cache. Read off the 3.2.1 source rather than assumed: one
PAPERLESS_REDIS url serves five consumers - the Celery broker, the result
backend (Redis in 3.x, the database before), the channels pub/sub layer and
two Django caches. allkeys-lru would not be a trade here but actively wrong,
because LRU evicts typeless across the whole keyspace and a backlog of
waiting consume tasks is exactly what it reads as cold; a dropped task is a
document silently never processed. The price, named in values.yaml rather
than smoothed over: two of the five consumers are caches, so a full instance
refuses writes - new tasks included - and the answer is memory sizing, not a
policy change. All three cache-like consumers are TTL-bounded, which is what
keeps that price small.

The password comes from the SAME item as the Redis connection, in a third
field auth-conf holding the complete line "requirepass <password>". It cannot
DEFAULT to secrets.redisSecretRef, and that is a property of Helm, not an
omission: a values.yaml is never templated and a parent cannot compute a
subchart's value. Measured - a template expression there lands verbatim in
the subchart's itemPath and fails as a YAML parse error naming neither the
value nor the cause.

Forgetting it is therefore caught in values.schema.json, by a conditional
rule rather than a fail() in a template. Also measured, and the reason:
Helm renders a subchart's templates BEFORE the parent's, so a guard in this
chart loses the race to the subchart's own required(), which reports
"secrets.authSecretRef" and cannot know the path belongs to the redis item.
A check that cannot fire is the mistake found while testing #77, so the
first draft of that guard was removed rather than shipped. The schema runs
before any rendering and names /valkey/secrets/authSecretRef.

One trap found while testing and closed here: "valkeySubchart: null" DELETES
the key, Helm then cannot resolve the condition path and treats the
dependency as ENABLED - so erasing the switch turned the subchart on with no
password set. Unlike every other block in this chart (issue #99), that one
is rejected by the schema instead of defaulting.

The install-test now runs with the subchart ON and the throwaway redis
Deployment removed: the url in the fixtures names ci-valkey-service, so the
bundled Valkey is the broker the release actually uses. #65 covered this
path by a render only - deliberately, since an unused Valkey would have made
that test look like it covered clustering. This is the first real
installation of the valkey-subchart path.

Documented, not glossed: the password now sits in that item twice and a
rotation has to touch both fields; with the subchart on the vault url is a
mandatory step, not a hint; and a OnePasswordItem materializes every field
of its item, so both Secrets carry all three keys - untidy, harmless, and
written down so nobody "tidies it up".

Fixes #78
This commit was merged in pull request #80.
This commit is contained in:
jklein committed 2026-10-02 21:24:22 +00:00
1 parent a288ee231a
commit a9ca5302de
10 files changed
+520 -56

No files matched your search

+1 -1
View File
@@ -117,4 +117,4 @@ The binding style guide for all current and future charts lives in **README.md (
- Resources: requests as values defaults; limits auto-computed by the `_helpers.tpl` helper (`limitFactor` default 3; no CPU limits unless set explicitly).
- Replicas per workload (explicit `0` allowed) plus a chart-global `scaleDown` flag that overrides them.
- `icon:` mandatory in every `Chart.yaml`; `helm lint --strict` without findings; CI fixtures under `ci/<chart>/` or a justified exclusion.
- **Chart dependencies are admissible but expensive** (the former "no dependencies" rule fell with #214; `ytdlp-web-player` was the first to use one, `stalwart` the second). A dependency has to be optional and off by default via `condition:`, and upstream's chart is consumed as it comes — where its defaults fall short of these conventions, that is **documented in our `values.yaml`, never patched**. The price to know before adding the next one: a subchart is invisible to everything downstream that watches Helm release versions, so a new upstream version — a security fix included — reaches a cluster only when the outer chart is republished (Renovate's built-in `helmv3` manager plus `ci/renovate-bump-chart.sh` is the whole safety net, and it belongs in a comment at the pinned version); and every helm command exits 1 while `charts/` is unfilled, which is why `ci/helm-dependencies.sh` runs before lint, package, render and install — including the render test that covers **all** charts on **every** PR. Full rules and the rest of the cost: README, "Chart dependencies".
- **Chart dependencies are admissible but expensive** (the former "no dependencies" rule fell with #214; `ytdlp-web-player` was the first to use one, `stalwart` the second, `paperless-ngx` the third). A dependency has to be optional and off by default via `condition:`, and upstream's chart is consumed as it comes — where its defaults fall short of these conventions, that is **documented in our `values.yaml`, never patched**. The price to know before adding the next one: a subchart is invisible to everything downstream that watches Helm release versions, so a new upstream version — a security fix included — reaches a cluster only when the outer chart is republished (Renovate's built-in `helmv3` manager plus `ci/renovate-bump-chart.sh` is the whole safety net, and it belongs in a comment at the pinned version); and every helm command exits 1 while `charts/` is unfilled, which is why `ci/helm-dependencies.sh` runs before lint, package, render and install — including the render test that covers **all** charts on **every** PR. Full rules and the rest of the cost: README, "Chart dependencies".
+6 -4
View File
@@ -25,7 +25,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
| `minecraft-router` | itzg/mc-router | Deployment + routing ConfigMap + NodePort Service (TCP only, no ingress); `mappings` is a required value |
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). **Only the newest Java line is maintained** (chart 5.0.0, #158): the directory carries one line, older ones stay published as frozen artifacts (java8/java17 at `4.0.0`, java21 at `4.1.0`) and receive no further chart changes — publishing once per line out of one directory is what made the lines drift. To run an older JVM with the current chart, set `image.tagOverride`; the chart then checks that `minecraft.version` fits the Java line of the **effective** tag and fails the render with both values named. Two cases pass unchecked on purpose: a tag without a `java<N>` part and a `minecraft.version` that is not a plain numeric `x[.y[.z]]`. `image.tagOverride` is deliberately outside Renovate's reach — `renovate.json5` tracks `appVersion` only. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21` / `-java25`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
| `outline` | Outline | StatefulSet, separate DB/Redis secrets |
| `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap |
| `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap. Optional `valkey` subchart — **this repo's own chart, from the published index**, off by default and gated on `valkeySubchart.enabled` (not `valkey.enabled`, which the subchart's strict schema rejects). Configured as a **queue** (PVC + `noeviction`), unlike `stalwart`'s cache: `PAPERLESS_REDIS` is the Celery broker, and `allkeys-lru` evicts across the whole keyspace, where a backlog of waiting consume tasks is exactly what LRU reads as cold. The password comes from the **same 1Password item** as the Redis connection, in a third field `auth-conf` holding the complete line `requirepass <password>` — so the password sits in that item **twice** and a rotation has to touch both fields. Two more traps: with the subchart on, `redis-connection-url` in the vault **must** be repointed at `<release>-valkey-service:6379` or the bundled Valkey runs unused, and `valkeySubchart:` must not be erased — a null override deletes the key, Helm reads an unresolvable `condition:` as *enabled*, so erasing the switch turns the subchart **on** (rejected by `values.schema.json`). Never change `valkey/` and this chart in the same PR, see "Chart dependencies" |
| `patchmon` | PatchMon | One Deployment (the 2.x server serves frontend and `/api/*` from one process) plus an optional guacd sidecar; OIDC-only sign-in |
| `rallly` | Rallly | Deployment, DB/OIDC/SMTP secrets |
| `rss-bridge` | RSS-Bridge | Deployment + whitelist ConfigMap; `ingress.hosts` is a required list of host + optional `tlsSecretName`, so several hosts can keep their own existing certificates (documented deviation from the one-TLS-secret rule, see its `values.yaml`) |
@@ -39,7 +39,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
| `urlaubsverwaltung` | Urlaubsverwaltung | Deployment, DB + OIDC secrets |
| `valkey` | Valkey | One instance per application (cache or queue backend), replacing the shared `cluster-redis` whose single ACL file coupled six applications together. 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 `include`d by the config — never in argv, never in the environment; an init container refuses to start the pod unless that line is really there, because the alternative failure is an open server that looks healthy. `maxmemory` is **derived from the computed memory limit** and a configured value above it aborts the render. Persistence and eviction are a pair, not two switches (queue = PVC + `noeviction`, the default; cache = emptyDir + `allkeys-lru`). First chart carrying a **NetworkPolicy** (on by default, Gitea #5), and the first whose probes had to be proven capable of failing: `valkey-cli` exits 0 on an error reply unless `-e` is given |
| `voucher-vault` | VoucherVault | StatefulSet, DB + OIDC secrets |
| `ytdlp-web-player` | ytdlp_web_player | Stateless Deployment, video cache and tool cache as emptyDirs (no PVC). **The first chart with a dependency** (`stalwart` is the second): an optional `oauth2-proxy` subchart, off by default, wired in front of the app because the application ships no authentication at all — see "Chart dependencies" below |
| `ytdlp-web-player` | ytdlp_web_player | Stateless Deployment, video cache and tool cache as emptyDirs (no PVC). **The first chart with a dependency** (`stalwart` and `paperless-ngx` followed): an optional `oauth2-proxy` subchart, off by default, wired in front of the app because the application ships no authentication at all — see "Chart dependencies" below |
| `zipline` | Zipline | StatefulSet, DB secret |
## Versioning
@@ -65,7 +65,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
Note: the registry **accepts a re-upload of an already published version** (HTTP 201) and replaces the stored artifact, so a republish of an existing version is still silent — see #160. That is also what makes a manual `workflow_dispatch` of any `publish-<chart>.yml` a harmless way to prove the shared block works.
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but one). It runs in **five** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but three: `ytdlp-web-player`, `stalwart`, `paperless-ngx`). It runs in **five** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix, or run the all-charts install-test after the merge. The **resource-quantity render test** is the exception to that rule: it runs over every chart on every PR — see below.
- `install-test.yml` — reusable workflow (`workflow_call`) holding the install-test **job** itself: the runner-label expression, the pinned `kubectl` with its skew check, `envsubst`, the containerised-job detection, the per-leg k3d cluster and the `freesailarr` VPN credentials. Its one input is a JSON array of chart names; its callers decide *which* charts are tested and whether the job runs at all, never *how*. Two callers: `validate-charts.yml` and `install-test-all-charts.yml`. **It is not copied for the second caller** — a second copy is a second chance to test differently, unnoticed, the same argument that gives `ci/excluded-charts.txt` exactly one parser.
@@ -440,7 +440,9 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
Until `ytdlp-web-player` (#214) this section read "charts have no dependencies (`charts/` directories are empty)". That rule is gone, replaced by a bar rather than a ban — a dependency is **admissible, not free**, and the reason it stopped being a ban is worth keeping: the chart needed an oauth2-proxy in the data path in front of an application that ships no authentication at all, and proxy and app are only useful wired to each other.
Two charts carry one, and comparing them is the quickest way to see where the bar sits. `ytdlp-web-player` takes **upstream's** `oauth2-proxy` because the app has no authentication and the two are useless apart; there, point 2 above applies in full. `stalwart` (Gitea #65) takes **one of our own published charts**, `valkey`, because Stalwart's **cluster coordinator rides on the in-memory store connection**, so a multi-node install does not work without one. Both are `condition:`-gated and off by default — for `stalwart` the subchart is the path for an installation that has no Valkey yet, and one that already has points `externalServices.valkey.host` at it so the dependency renders nothing.
Three charts carry one, and comparing them is the quickest way to see where the bar sits. `ytdlp-web-player` takes **upstream's** `oauth2-proxy` because the app has no authentication and the two are useless apart; there, point 2 above applies in full. `stalwart` (Gitea #65) takes **one of our own published charts**, `valkey`, because Stalwart's **cluster coordinator rides on the in-memory store connection**, so a multi-node install does not work without one. `paperless-ngx` (Gitea #78) takes the same one, because `PAPERLESS_REDIS` is the Celery broker and the application therefore cannot run without a Redis at all — before the subchart, every installation needed a second Helm release whose address lived in a 1Password item and in neither chart. All three are `condition:`-gated and off by default: the subchart is the path for an installation that has no Valkey yet, and one that already has points its connection value at that instead, so the dependency renders nothing.
**The same subchart, opposite configuration — and that comparison is the useful part.** `stalwart` runs it as a **cache** (`persistence.enabled: false` + `allkeys-lru`): its in-memory store holds rate-limiter counters, tokens and pub/sub, all reconstructible, and the cost it accepts is that the limiters fail open under memory pressure. `paperless-ngx` runs it as a **queue** (PVC + `noeviction`, the subchart's defaults): the broker's contents are work, and a dropped consume task is a document that is silently never processed. `allkeys-lru` would not be a trade there but actively wrong, because LRU evicts typeless across the whole keyspace and a backlog of waiting tasks is exactly what it reads as cold. The price `paperless-ngx` accepts instead: the same instance also serves the Django cache and the Celery result backend (both Redis in 3.x), so a full instance **refuses writes** — new tasks included — rather than dropping cold keys, and the answer is memory sizing, not a policy change. The valkey chart treats the two values as a **pair** and warns against mixing them; these two charts are the worked examples of picking either half deliberately.
**A dependency on one of our own charts is cheaper in one way and more dangerous in another.** Point 2 stops applying: there are no third-party defaults to leave unpatched, because the subchart follows these guidelines by being one of ours. In exchange it adds a hazard an external dependency does not have, and it is sharp enough to be a rule:
+43 -42
View File
@@ -1,11 +1,19 @@
# Fixtures for the paperless-ngx install-test: throwaway postgres + redis,
# the secrets the 1Password operator would otherwise materialize (release
# name: ci), and storage plumbing. The chart hardcodes storageClassName
# Fixtures for the paperless-ngx install-test: a throwaway postgres, the
# secrets the 1Password operator would otherwise materialize (release name:
# ci), and storage plumbing. The chart hardcodes storageClassName
# "longhorn" (RWX for the consume PVC), which does not exist in k3d: a
# no-provisioner StorageClass named "longhorn" plus pre-bound hostPath PVs
# stand in for it. Paperless runs as uid 1000 with a read-only rootfs and
# hostPath volumes do not support fsGroup, so a node-setup Job pre-creates
# the exact subPath directories world-writable.
#
# THERE IS NO THROWAWAY REDIS HERE ANY MORE, and that is the change this file
# carries: the release brings its own Valkey through the subchart
# (ci/paperless-ngx/test-values.yaml says what that proves), so the broker under
# test is the chart's own object rather than a Deployment next to it. Leaving a
# redis Deployment in place would have been the worse of the two: the url would
# have had to name one of them, and whichever it named, the other would have sat
# there making the run look like it covered something it did not.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
@@ -76,12 +84,43 @@ spec:
path: /var/lib/ci-pv/paperless-ngx/consume
type: DirectoryOrCreate
---
# The two Secrets below stand in for ONE 1Password item. Both the chart's
# redis OnePasswordItem and the subchart's auth OnePasswordItem point at
# op://ci/paperless-redis, and a OnePasswordItem materializes EVERY field of the
# item it names - so in a real cluster both Secrets carry all three fields. They
# are written out that way here rather than trimmed to what each consumer reads,
# because the trimmed version would have been a fixture that is tidier than
# reality and would have hidden the only thing worth knowing: the extra keys are
# unused, not a leak and not a bug.
apiVersion: v1
kind: Secret
metadata:
name: ci-paperless-ngx-redis-secret
stringData:
redis-connection-url: redis://:cipass@redis:6379
# Names the SUBCHART's Service, which is the mandatory vault step of this
# configuration (NOTES.txt prints it). A wrong name here is the failure this
# install-test exists to catch: paperless would come up healthy and process
# nothing.
redis-connection-url: redis://:cipass@ci-valkey-service:6379
key-prefix: ci
# Same password as in the url above - that duplication is the chart's
# documented trap 1, and the fixture is the cheapest place to keep it honest:
# let the two drift and the run goes red on authentication.
auth-conf: requirepass cipass
---
# What the subchart's own OnePasswordItem (ci-valkey-secret) would become. The
# key the Valkey actually mounts is "auth-conf", and its value has to be a
# COMPLETE configuration line, because the Valkey configuration includes the
# file: give it anything that is not "requirepass <password>" and the init
# container refuses to start the pod, which is the failure that chart exists to
# make loud.
apiVersion: v1
kind: Secret
metadata:
name: ci-valkey-secret
stringData:
auth-conf: requirepass cipass
redis-connection-url: redis://:cipass@ci-valkey-service:6379
key-prefix: ci
---
apiVersion: v1
@@ -153,41 +192,3 @@ spec:
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: public.ecr.aws/docker/library/redis:7-alpine
args: ["redis-server", "--requirepass", "cipass"]
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
+69
View File
@@ -1,4 +1,36 @@
# CI values for the paperless-ngx install-test.
#
# THE SUBCHART IS ON, AND THAT IS THE POINT OF THIS FILE. Gitea #65 added the
# same valkey dependency to stalwart but covered it by a local RENDER only -
# deliberately, because Stalwart's in-memory store is an object in its database,
# so a Valkey in that namespace would have been reachable and unused and the
# test would have looked like it covered the clustering path while covering
# nothing of it. Here it is the opposite: paperless-ngx reads PAPERLESS_REDIS
# from the vault item, so pointing that url at the subchart's Service makes the
# bundled Valkey the broker this release actually uses. So this is the first
# install of the valkey-subchart path anywhere, and what it proves is:
#
# 1. The dependency resolves from the published index on a real runner with no
# credentials, and lint/package/render/install all survive it.
# 2. The subchart's objects are accepted inside a parent release: its
# StatefulSet becomes Ready, which means its init container found a
# "requirepass " line in the mounted auth file and its three exec probes
# authenticated and got PONG back.
# 3. The PVC path of the subchart works under a parent release name
# (ci-valkey-*), i.e. the names really are <release>-valkey-<kind>.
# 4. paperless-ngx talks to it: the url in the fixture Secret names
# ci-valkey-service, so a wrong Service name or a password mismatch between
# the two fields of the "item" shows up as a failing run instead of as a
# quietly idle Valkey.
#
# What it still CANNOT prove is that documents are processed - that needs a real
# consume run and is out of scope here, as before.
secrets:
# One item for both, which is the configuration the chart is designed around:
# redis-connection-url, key-prefix AND auth-conf live in the same 1Password
# item. In CI the operator does not exist, so ci/paperless-ngx/fixtures.yaml
# stands in for it - and it stands in TWICE, because a OnePasswordItem
# materializes every field of the item it names. See the fixture.
redisSecretRef: op://ci/paperless-redis
databaseSecretRef: op://ci/paperless-db
paperlessSecretRef: op://ci/paperless
@@ -16,5 +48,42 @@ paperless:
# neither erase them nor break rendering.
barcodes:
valkeySubchart:
enabled: true
valkey:
secrets:
# The SAME op:// path as secrets.redisSecretRef above, typed out again
# because Helm cannot default one value from another (Gitea #78). Typing it
# here is also what exercises the parent's guard in the "satisfied"
# direction; the failing direction is covered by a local render, since the
# harness installs one configuration per chart.
authSecretRef: op://ci/paperless-redis
persistence:
# QUEUE, the chart's and the subchart's default - and the risky half, which
# is why it is not switched off here: a provisioned PVC, the pod's fsGroup
# against a volume it did not create, and an append-only file written and
# replayed on it.
enabled: true
appendonly: true
# k3d's dynamic provisioner. The subchart's default, longhorn-encrypted-ns,
# does not exist on the runner, and the no-provisioner "longhorn" class the
# paperless fixtures define is pre-bound to hostPath PVs for paperless' own
# claims - a hostPath volume gets no fsGroup from the kubelet at all, so
# reusing it here would silently stop testing the ownership path.
storageClass: local-path
size: 128Mi
valkey:
resources:
# Small, to stay inside the runner's budget, while still exercising the
# subchart's derivation: 64Mi x limitFactor 3 = 192Mi limit, maxmemory =
# 0.75 x that = 144Mi.
requests:
cpu: 50m
memory: 64Mi
ephemeral-storage: 32Mi
# smbConnection and oidc stay disabled (defaults): the SMB CSI driver and a
# real OIDC login flow are out of scope for the CI install-test.
+78 -1
View File
@@ -5,9 +5,86 @@ icon: https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/dev/src-ui/s
type: application
version: 12.1.3+up3.2.1
version: 12.2.0+up3.2.1
appVersion: 3.2.1
maintainers:
- name: jklein
# The third chart in this repo with a dependency (after ytdlp-web-player and
# stalwart). README, "Chart dependencies", holds the bar a dependency has to
# clear and the full price list; what is repeated here is only what is specific
# to THIS chart, because the generic half is already written down twice.
#
# WHY ONE HERE. paperless-ngx cannot run without a Redis: PAPERLESS_REDIS is
# the Celery broker, and without it the web UI comes up looking healthy while
# consumption, OCR, the scheduled tasks and the index updates never run. So
# every installation needs one, and before this block the only way to have one
# was a second Helm release whose address lived in a 1Password item and in
# neither chart.
#
# WHY IT IS OFF BY DEFAULT: an installation that already has a Redis (or wants
# a shared one, which is what secrets.redisSecretRef' key-prefix field exists
# for) points redis-connection-url at it, and this dependency renders nothing.
# The subchart is the path for an installation that has none.
#
# THE SUBCHART IS OUR OWN valkey CHART, resolved from the published index - not
# upstream's. So README guideline 14.2 ("we consume their chart as it comes")
# does not apply and there are no third-party defaults to leave unpatched: the
# subchart follows this repo's conventions because it IS this repo's chart.
# What does apply in full is the subchart being invisible to everything
# downstream that watches Helm release versions, and the PR-ordering hazard
# below.
dependencies:
- name: valkey
# KEEP THIS VERSION MOVING - it is the only path a Valkey update has into a
# cluster through this chart. The version declared here is baked into our
# packaged .tgz, so anything watching Helm release versions (Fleet
# included) sees only "paperless-ngx": a new valkey chart, a security fix
# included, arrives ONLY when this line is raised and a new paperless-ngx
# version is published. Nothing reports the gap.
#
# Renovate's BUILT-IN helmv3 manager reads this block and opens the PR, and
# ci/renovate-bump-chart.sh bumps this chart's patch version, which is what
# actually ships it. No customManagers entry exists or would help - that
# manager is for exactly this case, where repository and version sit
# together in one file. Measured for Gitea #65 against our self-hosted
# index (Renovate 44.132.2, dry run with the dependency pinned low:
# newVersion "1.0.0+up9.1.2.alpine", versioning "helm"), so the chain is
# proven for this registry and this build-metadata suffix and was not
# re-measured here.
#
# Independent of appVersion: raising it is a patch or minor bump of this
# chart and is never tied to the paperless-ngx image.
version: "1.0.0+up9.1.2.alpine"
# Our own published index, and the READ path only - anonymously readable
# (measured), so ci/helm-dependencies.sh needs no credential and was not
# touched. The publish target and its credentials still live in 1Password
# and appear nowhere in this repository; see README, "CI/CD".
repository: "https://git.jk-dev.de/api/packages/jlab-cluster/helm"
# Off by default, so a plain install renders none of its objects.
#
# NOT "valkey.enabled", AND THAT IS NOT A STYLE CHOICE. Everything under
# the "valkey:" key is handed to the subchart as ITS values and validated
# against ITS schema, and our valkey chart ships additionalProperties:
# false with no "enabled" property:
# Error: values don't meet the specifications of the schema(s) in the
# following chart(s): valkey: - at '': additional properties 'enabled'
# not allowed
# Worse, Helm skips a DISABLED subchart's schema validation, so with the
# default of false that error would stay invisible through lint, render and
# install-test and surface only for whoever first switched it on - the one
# case the value exists for. The condition is therefore a key of OUR OWN,
# outside the subchart's value subtree.
condition: valkeySubchart.enabled
#
# !!! NEVER CHANGE valkey/ AND THIS CHART IN THE SAME PULL REQUEST !!!
#
# The dependency resolves out of the PUBLISHED index, not out of the working
# tree, and valkey/ is published only by a push to main that touches it. A
# single PR carrying both therefore runs lint, package, render AND the
# install-test against the OLD valkey .tgz and goes green about an artifact
# that is not the one in the diff. Two consecutive pull requests: change
# valkey/, merge, let it publish, then raise the version here. README, "Chart
# dependencies", and the wiki's valkey page carry the long form.
+78 -4
View File
@@ -3,6 +3,9 @@
{{- $barcodes := fromYaml (include "paperless-ngx.barcodes" .Values.paperless.files.barcodes) -}}
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
{{- $scripts := .Values.paperless.scripts | default dict -}}
{{- $valkeySub := (.Values.valkeySubchart | default dict).enabled -}}
{{- $valkeyAuth := ((.Values.valkey | default dict).secrets | default dict).authSecretRef -}}
{{- $valkeySvc := printf "%s-valkey-service" .Release.Name -}}
{{- $pre := default (list) $scripts.preConsumptionScripts -}}
{{- $post := default (list) $scripts.postConsumptionScripts -}}
Paperless-ngx {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
@@ -70,6 +73,12 @@ WHAT THE 1PASSWORD ITEMS MUST CONTAIN
key-prefix PAPERLESS_REDIS_PREFIX — set this per release when
several releases share one Redis, or two instances
will consume each other's task queue
auth-conf the COMPLETE line "requirepass PASSWORD", not a
bare password. Read by the bundled Valkey only, so
it is {{ if $valkeySub }}REQUIRED for this release — see the Valkey
section below{{ else }}unused while valkeySubchart.enabled is
false, and the item needs it only once you switch
the bundled Valkey on{{ end }}.
secrets.paperlessSecretRef → {{ .Values.secrets.paperlessSecretRef }}
Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, fields:
@@ -117,9 +126,9 @@ CHANGING A SECRET'S CONTENT RESTARTS NOTHING
fields points the release at whatever that new endpoint contains, empty or
not, and the next start migrates it.
POSTGRES AND REDIS ARE NOT PART OF THIS CHART
Both have to exist and be reachable before the first start. They fail in
opposite ways, and only one of them is loud:
POSTGRES IS NOT PART OF THIS CHART{{ if not $valkeySub }}, AND NEITHER IS REDIS{{ end }}
{{ if $valkeySub }}The database{{ else }}Both{{ end }} ha{{ if $valkeySub }}s{{ else }}ve{{ end }} to exist and be reachable before the first start.{{ if not $valkeySub }} They fail in
opposite ways, and only one of them is loud:{{ end }}
- Without the database paperless does not come up at all. All three probes
ask for "/" on port 8000, so you get no useful error: the pod never turns
@@ -129,7 +138,72 @@ POSTGRES AND REDIS ARE NOT PART OF THIS CHART
broker: consumption, OCR, the scheduled tasks and the index updates simply
never run, and documents dropped into the consume directory stay there.
A wrong redis-connection-url costs you the whole processing side and
nothing turns red.
nothing turns red.{{ if $valkeySub }} This release brings its own Valkey (below), so
the address is in your hands — which is exactly why the url is the one
thing to check first.{{ end }}
{{ if $valkeySub }}
THIS RELEASE BRINGS ITS OWN VALKEY — ONE MANDATORY STEP IS IN THE VAULT
valkeySubchart.enabled is true, so the release contains a Valkey of its own:
StatefulSet {{ .Release.Name }}-valkey-sfs, Service {{ $valkeySvc }}, its own
NetworkPolicy, and a PersistentVolumeClaim (persistence is on — this is a
queue, see below).
1. THE CONNECTION URL MUST POINT AT IT. This is a step, not a hint: the url
lives in the vault and the chart cannot write it. In the item
"{{ .Values.secrets.redisSecretRef }}", field redis-connection-url:
redis://:PASSWORD@{{ $valkeySvc }}:6379
Pointing elsewhere is not an error anywhere: paperless talks to that other
Redis, the Valkey next to it runs along unused, and nothing reports it.
2. THE PASSWORD IS IN THE ITEM TWICE — once inside redis-connection-url and
once in the "requirepass PASSWORD" line of auth-conf. Both are the same
password, and a rotation has to touch BOTH fields. Change only one and
paperless can no longer authenticate against its own Valkey. After any
such change, restart both workloads by hand (Helm cannot see inside a
1Password item, so nothing rolls automatically):
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-valkey-sfs
3. AUTH-CONF IS A CONFIGURATION LINE, NOT A PASSWORD. The Valkey mounts the
field as a file and includes it, so the password never reaches a process
list or an environment variable. An init container refuses to start the
pod unless a line beginning with "requirepass " is really there — without
that check a typo would leave a Valkey running with NO password and
looking perfectly healthy.
{{- if and $valkeyAuth (ne ($valkeyAuth | toString) ($.Values.secrets.redisSecretRef | toString)) }}
NOTE: valkey.secrets.authSecretRef points at a DIFFERENT item than
secrets.redisSecretRef:
valkey.secrets.authSecretRef → {{ $valkeyAuth }}
secrets.redisSecretRef → {{ .Values.secrets.redisSecretRef }}
That is supported and may well be deliberate — the bundled Valkey is then fed
from its own item. It is only worth knowing that you now keep TWO items in
step: the password in auth-conf over there and the one inside
redis-connection-url here have to stay identical.
{{- end }}
BOTH ITEMS' FIELDS LAND IN BOTH SECRETS. A OnePasswordItem materializes every
field of the item it names, so with one shared item the Valkey's Secret also
carries redis-connection-url and key-prefix, and this chart's redis Secret
also carries auth-conf. Untidy and harmless — nothing mounts a field it does
not reference. It is mentioned so it does not get "cleaned up".
IT IS CONFIGURED AS A QUEUE, and that is a decision: persistence on, eviction
policy "noeviction". PAPERLESS_REDIS is the Celery broker, so its contents are
work that must not vanish — "allkeys-lru" would evict across the whole
keyspace and a backlog of waiting tasks is exactly what LRU reads as cold.
The price: the same instance also serves the Django cache, so when it fills,
writes are REFUSED rather than old keys dropped — new tasks included. The
answer is to give it more memory (valkey.valkey.resources.requests.memory),
never to switch the policy. values.yaml argues this in full.
SCALEDOWN DOES NOT REACH IT. scaleDown: true scales paperless to zero and
leaves the Valkey running, which is the right way round for a queue: the
backlog stays where it is.
{{- end }}
THE FIRST START MIGRATES, AND THE STARTUP PROBE IS THE CLOCK
Every start runs the database migrations before the server answers, and the
+6 -4
View File
@@ -20,10 +20,12 @@ Usage (the values path to the resources dict varies per chart):
{{- include "paperless-ngx.resources" .Values.<app>.resources | nindent 12 }}
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. This file in template-chart is the reference
implementation.
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 does take one - the optional valkey subchart - which changes
nothing here: a library chart for one helper would still be the wrong trade,
and the subchart carries its own copy of this helper anyway. This file in
template-chart is the reference implementation.
*/}}
{{- define "paperless-ngx.resources" -}}
{{- $resources := . | default dict -}}
@@ -1,3 +1,12 @@
{{- /*
With the valkey subchart on, THIS is the item that also carries the "auth-conf"
field the bundled Valkey reads (NOTES.txt prints all three fields). The rule
that valkey.secrets.authSecretRef must then be set is enforced in
values.schema.json and NOT by a fail() here - measured, see that file: Helm
validates the schema before it renders, and it renders a subchart's templates
before the parent's, so a check in this file could never beat the subchart's own
required() to it.
*/ -}}
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
+57
View File
@@ -4,6 +4,48 @@
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key that the chart does not read is rejected at render time instead of being silently ignored. The only objects that stay open are the ones passed verbatim into the Kubernetes pod spec (probes, security contexts, global) - the chart does not interpret their contents, so it cannot judge which keys are meaningful. The hardening, probe and resources blocks of BOTH workloads, and the barcodes block from issue #61, additionally accept null, because an erased block falls back to the chart defaults (issues #61 and #99).",
"type": "object",
"additionalProperties": false,
"required": ["valkeySubchart"],
"allOf": [
{
"title": "With the valkey subchart switched on, valkey.secrets.authSecretRef is required",
"description": "The one cross-key rule in this file, and it lives here rather than in a template on purpose: Helm validates the schema BEFORE it renders anything, and it renders a subchart's templates before the parent's. A fail() in this chart's templates therefore loses the race against the subchart's own required(), whose message names 'secrets.authSecretRef' and cannot know that the path belongs to the same 1Password item as secrets.redisSecretRef. Measured for Gitea #78 - a guard that cannot fire is the mistake found while testing #77. Set it to the SAME op:// path as secrets.redisSecretRef: the bundled Valkey reads its password from the field 'auth-conf' in that item, whose value must be the complete line 'requirepass <password>'. A different item is allowed; two items then have to be kept in step.",
"if": {
"required": ["valkeySubchart"],
"properties": {
"valkeySubchart": {
"type": "object",
"required": ["enabled"],
"properties": {
"enabled": {
"const": true
}
}
}
}
},
"then": {
"required": ["valkey"],
"properties": {
"valkey": {
"type": "object",
"required": ["secrets"],
"properties": {
"secrets": {
"type": "object",
"required": ["authSecretRef"],
"properties": {
"authSecretRef": {
"type": "string",
"minLength": 1
}
}
}
}
}
}
}
}
],
"properties": {
"global": {
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
@@ -261,6 +303,21 @@
}
}
}
},
"valkeySubchart": {
"description": "The dependency condition for the valkey subchart. A key of THIS chart, deliberately outside the 'valkey' subtree: everything under 'valkey:' is handed to the subchart as its values, and this repo's valkey chart has additionalProperties: false with no 'enabled' property, so 'valkey.enabled' is rejected by the subchart itself. Helm skips a disabled subchart's schema validation, so that error would only ever have surfaced for whoever first set it to true. NOT null-safe and 'enabled' is REQUIRED, which is the opposite of this chart's other blocks (issue #99) and is deliberate: Helm treats a condition whose path does not resolve as ENABLED, so an erased 'valkeySubchart:' would switch the subchart ON - measured for Gitea #78, where it surfaced as the subchart's own required() error. An erased block must not flip behaviour, least of all in that direction, so it is rejected here instead.",
"type": "object",
"additionalProperties": false,
"required": ["enabled"],
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"valkey": {
"description": "Values passed through to the valkey subchart - this repo's OWN valkey chart, resolved from the published index. Open on purpose and at every level: the subchart owns this schema (it ships its own, and a stricter one than this), and a closed copy here would reject whatever that chart adds in its next release while duplicating a validation that already happens. This chart reads NONE of these keys; the dependency condition is valkeySubchart.enabled instead. Two shapes that are easy to get wrong: secrets.authSecretRef must be set BY HAND to the same op:// path as secrets.redisSecretRef (a values.yaml is never templated, so a parent cannot default it - Gitea #78), and the item's field 'auth-conf' holds the complete line 'requirepass <password>', not a bare password. values.yaml argues the queue-vs-cache choice and names the three operator traps.",
"type": "object"
}
},
"definitions": {
+173
View File
@@ -5,6 +5,10 @@
scaleDown: false
secrets:
# op:// path of the item holding the Redis/Valkey connection. Fields:
# "redis-connection-url" and "key-prefix" always, plus "auth-conf" when the
# bundled Valkey subchart is used - see the valkeySubchart block at the end
# of this file. NOTES.txt prints all three.
redisSecretRef: null
databaseSecretRef: null
paperlessSecretRef: null
@@ -176,3 +180,172 @@ oidc:
# Some providers need this since the allauth update in paperless-ngx 3.0
# (login fails at the callback with invalid_client otherwise).
tokenAuthMethod: null
# ---------------------------------------------------------------------------
# The bundled Valkey - this repo's own valkey chart as an optional subchart
# ---------------------------------------------------------------------------
# The switch for the dependency declared in Chart.yaml. Off by default: an
# installation that already has a Redis points redis-connection-url at it and
# this renders nothing.
#
# WHY THE KEY IS HERE AND NOT "valkey.enabled": everything under "valkey:" is
# handed to the subchart as its values and validated against ITS strict schema,
# which has no "enabled" property - so "valkey.enabled" is rejected BY THE
# SUBCHART. And because Helm skips a disabled subchart's schema validation, the
# error would only ever have appeared for whoever first switched it on.
# Chart.yaml carries the exact error text.
#
# DO NOT ERASE THIS BLOCK. Unlike every other block in this file (issue #99),
# "valkeySubchart: null" is not "the default applies" - a null override DELETES
# the key, the dependency's condition path then does not resolve, and Helm
# treats an unresolvable condition as ENABLED. So erasing the switch turns the
# subchart ON, with no password set and only the subchart's own error to show
# for it. Measured for Gitea #78; values.schema.json rejects it before anything
# renders. An empty block ("valkeySubchart: {}") is fine, because Helm merges
# that with the default below.
valkeySubchart:
enabled: false
# ---------------------------------------------------------------------------
# Values passed THROUGH to the valkey subchart
# ---------------------------------------------------------------------------
# Read as the SUBCHART's own values, so the key names are its schema, not this
# chart's. `valkey/values.yaml` in this repo is the reference. Nothing here has
# any effect while valkeySubchart.enabled is false.
#
# What still applies in full: the subchart is invisible to everything
# downstream that watches Helm release versions (Chart.yaml has the detail and
# the PR-ordering warning - never change valkey/ and this chart in the same
# PR), and this chart's scaleDown flag does NOT reach it. scaleDown: true
# scales paperless to zero and leaves the Valkey running; that is the right way
# round for a queue, because the backlog stays where it is.
valkey:
secrets:
# REQUIRED once the subchart is on, and it has to be set BY HAND to the
# SAME op:// path as secrets.redisSecretRef above - because the
# "requirepass" line lives in that same item (see the three traps below).
#
# IT CANNOT DEFAULT TO IT, and that is a property of Helm, not an omission:
# a values.yaml is never templated and a parent chart cannot compute a
# subchart's value - values are merged statically before any template runs.
# Writing "{{ .Values.secrets.redisSecretRef }}" here does not resolve, it
# lands verbatim in the subchart's itemPath and fails as a YAML parse error
# pointing at the subchart's template, which names neither this value nor
# the real cause. Measured; Gitea #78 has the output. So the path is typed
# twice, and forgetting it is caught by a conditional rule in
# values.schema.json - NOT by a fail() in a template, which cannot work
# here: Helm renders a subchart's templates BEFORE the parent's, so the
# subchart's own required() always wins that race and reports
# "secrets.authSecretRef" instead of this value's real path. The schema runs
# earlier than any rendering, which is why the rule lives there.
#
# MIND THE FIELD, it is not a password. The item must carry a field
# "auth-conf" whose value is the COMPLETE configuration line
# requirepass <password>
# The valkey chart mounts that field as a file and `include`s it, so the
# password never reaches the process list, an environment variable, or
# Helm's field of view. An init container refuses to start the pod unless
# the line is really there, because the alternative failure is an open
# server that looks perfectly healthy. A bare password does not work.
#
# OVERRIDING IT WITH A DIFFERENT ITEM IS ALLOWED and is not a mistake: it
# is the way to feed the bundled Valkey from its own item. The cost is that
# two items then have to be kept in step instead of one.
authSecretRef: null
# QUEUE, not cache - a decision, with the conflict it does not resolve.
#
# The valkey chart treats persistence and eviction as a PAIR and warns
# against mixing them: QUEUE is persistence.enabled: true + "noeviction"
# (its default), CACHE is persistence.enabled: false + "allkeys-lru".
# stalwart picked CACHE. paperless-ngx is the other case - which happens to
# be the subchart's own default, so the block below changes nothing. It is
# written out anyway, for two reasons: the subchart's persistence block is
# deliberately NOT null-safe (an erased one renders an invalid PVC), and the
# decision belongs where it is argued rather than one chart away. The
# eviction policy is NOT restated - valkey.maxmemoryPolicy stays at the
# subchart's "noeviction", which is the queue half of the pair. storageClass
# stays at the subchart default too (longhorn-encrypted-ns): queue payloads
# are the same documents that are in the database, so encrypted at rest.
#
# Read off the paperless-ngx 3.2.1 source (src/paperless/settings/__init__.py
# and settings/custom.py), not assumed: PAPERLESS_REDIS is ONE url, and
# parse_redis_url returns it twice for a TCP url, so one instance carries
# five consumers:
#
# 1. CELERY_BROKER_URL - the queue: consume, OCR, index
# updates, classifier training, the
# scheduled tasks
# 2. CELERY_RESULT_BACKEND - also Redis in 3.x (it was the
# database before), CELERY_RESULT_EXPIRES
# 3600
# 3. CHANNEL_LAYERS - channels_redis.pubsub, expiry 15s;
# pub/sub, not stored keys
# 4. CACHES["default"] - Django cache, recomputable
# 5. CACHES["read-cache"] - Cachalot DB read cache, TTL 3600,
# off unless
# PAPERLESS_DB_READ_CACHE_ENABLED
#
# WHY QUEUE: "allkeys-lru" would not be a trade here, it would be actively
# wrong. LRU evicts across the WHOLE keyspace with no regard for key type,
# and a backlog of waiting consume tasks is exactly what LRU reads as cold -
# written once, read later. So a cache configuration would preferentially
# throw away the work and keep the cache entries. A dropped consume or index
# task means a document that is silently never processed: the file stays in
# the consume directory although the directory watcher has already fired.
# And an evicted Celery result means a task that shows as "started" in the UI
# forever.
#
# THE CONFLICT THAT STAYS, stated rather than smoothed over: two of the five
# consumers are caches and they get the policy that does not suit them. When
# the instance fills, "noeviction" makes Valkey REFUSE WRITES - which hits
# cache writes and NEW TASKS alike, so paperless starts erroring instead of
# quietly degrading. That is the loud direction and therefore the right one,
# but it is a price and not a side effect, and the answer to it is to SIZE
# MEMORY for the working set, never to switch the policy. What makes the
# price small: all three cache-like consumers are TTL-bounded (15s, 3600s,
# 3600s), so they bound themselves and do not need eviction to stay in check,
# and the read cache is off by default.
persistence:
enabled: true
appendonly: true
# Sized for a task backlog and its results, not for documents - the
# documents are on paperless' own PVC. Grow it if a release regularly
# queues thousands of tasks (a PVC can grow, it can never shrink).
size: 1Gi
# networkPolicy and the Valkey resources are left at the subchart's defaults
# on purpose: its NetworkPolicy is on and allows the release's own namespace,
# which with a subchart is where paperless runs. allowedNamespaces only needs
# an entry when the two are in DIFFERENT namespaces, which a subchart rules
# out.
# ---------------------------------------------------------------------------
# THE THREE TRAPS OF THE BUNDLED VALKEY - all three are operator work
# ---------------------------------------------------------------------------
# 1. THE PASSWORD IS IN THE ITEM TWICE: inside redis-connection-url and again
# in the "requirepass" line of auth-conf. A rotation has to touch BOTH
# fields; touching one leaves a paperless that cannot authenticate against
# its own bundled Valkey. The alternative - assembling the url from parts -
# would break the secret contract of every running release (a major) and
# route the password through a template. Deliberately not chosen; the trap
# is named instead.
#
# 2. WITH THE SUBCHART ON, redis-connection-url MUST POINT AT THE SUBCHART:
# redis://:<password>@<release>-valkey-service:6379
# The name is determined and confirmed against the rendered output, but the
# url lives in the vault, so this is a MANDATORY STEP of the installation
# and not a hint. Left pointing at an external Redis, the bundled Valkey
# runs along unused and nothing says so. NOTES.txt prints the exact name for
# the release at hand.
#
# 3. A OnePasswordItem MATERIALIZES EVERY FIELD OF THE ITEM. With both CRs
# pointing at the same item, the Valkey Secret therefore also carries
# redis-connection-url and key-prefix, and this chart's redis Secret also
# carries auth-conf. Untidy, harmless, and written down here so nobody reads
# it as a leak or a bug and "tidies it up": the keys are unused, and no
# container mounts a field it does not reference.
#
# And one rule that is about this repository rather than about an installation:
# changes to valkey/ must not share a pull request with changes to this chart.
# Chart.yaml says why; README and the wiki's valkey page carry the long form.