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
+521 -57

No files matched your search

+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.