Three issues on one chart, bundled because they share values.yaml, values.schema.json and the version line, and because the first half of #11 lives in the same template as #101. Gitea #11, first half: the consume CronJob's schedule was rendered bare, so a schedule in the commonest cron notation there is - a star, a slash, an interval - aborted the render with a YAML parse error pointing at the template while the operator's value appeared nowhere in the message. Quoting it in the values file does not help; Helm passes the content through. Reproduced before the fix, at the line the issue names, and fixed with | quote. Gitea #11, second half: secrets.oidcSecretRef is removed, values and schema both. No template ever read it, so no OnePasswordItem was created and nothing reached the pod - the quiet kind of defect that looks like successful configuration. It is not reimplemented, and the reason is in paperless: the whole OIDC configuration travels in one json environment variable with no _FILE variant, so a secretKeyRef cannot substitute a fragment into it. The operational question is answered by #83 - Keycloak marks a client public per client, so nothing forces a confidential one here. In the same breath, oidc.provider.tokenAuthMethod is restricted to "none". Its comment named client_secret_basic as the example, which asks the provider for an authentication this chart cannot perform: it configures no client secret and cannot. The two values read together looked like a working secret setup, and the symptom was a login dying at the callback with invalid_client. No live release sets it (measured against all three instances). Gitea #112 asked for an extraEnv and the chart already had the standard additive env block - the issue was measured against a deployed 12.2.0 and read only the top-level keys. What was actually broken is the block itself: it rendered its operand bare, so a "*"-leading value aborted the render and "true" or "0755" arrived as a bool or an integer, which the API server rejects after a green render. It is brought to the repository's documented quoted form (template-chart carries the rule), which is what makes an arbitrary PAPERLESS_* variable settable in practice. PAPERLESS_LOGGING_DIR is documented as the worked example, with the cross-reference it needs: the root cause is the NFS-exported RWX data volume in jk-cluster-config #360 and this does not fix it - it only moves the log off that volume. The two Ingress upload-size annotations had the same bare render and are quoted too. Gitea #101: the consume mover now runs on the node of the StatefulSet pod, via a required podAffinity on its label. It mounts the RWX consume volume, and Longhorn's CSI driver refuses that mount unless it finds a workload pod in Pending, so a mover on another node left the StatefulSet node's kubelet retrying every two minutes - 291 events in 10 hours in both releases with SMB on, none in the release without the CronJob. A static nodeSelector would be wrong because the StatefulSet pod is not pinned either, and preferred would reproduce the bug more rarely and harder to find. The cost of a required term is handled rather than left open: concurrencyPolicy: Forbid, a startingDeadlineSeconds that also keeps the controller away from its 100-missed-schedules stop, and an activeDeadlineSeconds that fails a job which cannot be placed instead of leaving one Pending pod per schedule. scaleDown already suspends the CronJob; what remains is paperless.replicas: 0 with SMB on, which now reports a bounded, visible failure. successfulJobsHistoryLimit is deliberately untouched: Longhorn's check asks for the existence of a Pending pod, not the absence of Succeeded ones, so those three pods are the error message's evidence and not its cause. Side defect fixed on the way: every read of smbConnection.cron dereferenced the block directly, so an override that had erased it aborted with a nil-pointer error naming an internal template path instead of falling back to the defaults this chart promises everywhere else. Measured on origin/main; all reads now go through a merged-defaults helper that preserves an explicit false. Major: a value is removed and fails the strict schema afterwards, and runtime placement changes. Fixes #11 Fixes #101 Fixes #112
543 lines
28 KiB
YAML
543 lines
28 KiB
YAML
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
|
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
|
|
# takes precedence over any per-workload replicas value. PVCs, Secrets and
|
|
# Services stay in place; scaleDown: false brings the workloads back up.
|
|
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
|
|
smbConnectionSecretRef: null
|
|
# REMOVED IN 13.0.0: secrets.oidcSecretRef (Gitea #11). It is gone from the
|
|
# schema too, so a release that still sets it now fails to render with
|
|
# additional properties 'oidcSecretRef' not allowed
|
|
# instead of being ignored - DELETE THE KEY from your values, nothing else is
|
|
# needed. It never did anything: no template read it, no OnePasswordItem was
|
|
# created for it, and whatever the item held never reached the pod.
|
|
#
|
|
# It is not coming back as a feature, and the reason is in paperless itself:
|
|
# the whole OIDC configuration travels in ONE json environment variable
|
|
# (SOCIALACCOUNT_PROVIDERS = json.loads(os.getenv(
|
|
# "PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"))), and there is no _FILE variant
|
|
# of it. A secretKeyRef cannot substitute a fragment INTO a json variable, so
|
|
# an honest implementation would have to either inline the secret in the pod
|
|
# spec or push the complete json into the vault by hand - and the second one
|
|
# makes every required() in the oidc block below dead wood.
|
|
#
|
|
# The operational question behind it is answered rather than dodged: this
|
|
# chart registers a PUBLIC client with PKCE, Keycloak (Gitea #83) can mark a
|
|
# client public per client, and so no provider constraint forces a
|
|
# confidential client here. See the oidc block below.
|
|
|
|
ingress:
|
|
# Set to false to omit the Ingress entirely. NEW in the chart version that
|
|
# added the httpRoute block below (Gitea #114): this chart used to render its
|
|
# Ingress unconditionally, which is why it has no issue-#93 switch like the
|
|
# rest. The default is true, so an existing release is unaffected.
|
|
#
|
|
# It was not added for uniformity. Without it the Ingress would keep
|
|
# rendering in Gateway mode, and with it the required() on clusterIssuer -
|
|
# a value demanded for something the chart then does not render. With the
|
|
# switch that required() is conditional, as everywhere else here.
|
|
#
|
|
# clusterIssuer becomes unnecessary with the Ingress off. ingress.domain does
|
|
# NOT: it is also PAPERLESS_URL, the only origin this release declares to
|
|
# Django, and it carries its own required() in the StatefulSet.
|
|
#
|
|
# Mutually exclusive with httpRoute.enabled below: with both true the render
|
|
# aborts, naming both values. Mind that enabled defaults to TRUE here, so
|
|
# turning the Gateway mode on means switching this off in the same values
|
|
# file.
|
|
enabled: true
|
|
clusterIssuer: null
|
|
# The public hostname of this release. Read by the Ingress, by PAPERLESS_URL
|
|
# in the StatefulSet (with its own required()), and - when
|
|
# httpRoute.hostnames is empty - by the HTTPRoute. Required in every mode.
|
|
domain: null
|
|
className: nginx
|
|
# Extra annotations for the Ingress; merged with the cert-manager
|
|
# cluster-issuer and upload-size annotations the chart always sets.
|
|
#
|
|
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
|
|
# annotation channel for controller behaviour, so anything configured here is
|
|
# simply gone when httpRoute.enabled is used instead - it is then the
|
|
# Gateway's business, or that of an implementation-specific policy object
|
|
# this chart deliberately does not render. For THIS chart that includes the
|
|
# upload cap: see paperless.files.maxUploadFileSize.
|
|
annotations: {}
|
|
|
|
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
|
|
# an existing release is unaffected; exactly one of ingress.enabled and
|
|
# httpRoute.enabled may be true - and ingress.enabled defaults to true, so it
|
|
# has to be switched off explicitly.
|
|
#
|
|
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
|
|
# gatewayClassName, no Certificate and no implementation-specific policy
|
|
# object. Class and certificate belong to the Gateway and therefore to the
|
|
# platform, which is what keeps this chart independent of whether the cluster
|
|
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
|
|
#
|
|
# WHAT THIS MODE COSTS HERE: paperless.files.maxUploadFileSize is not enforced,
|
|
# so the web UI's upload ceiling is the Gateway's. The consume directory is
|
|
# unaffected - it passes no proxy and has no limit at all.
|
|
#
|
|
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
|
|
# cannot detect them (.Capabilities.APIVersions is unreliable at template
|
|
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
|
|
httpRoute:
|
|
enabled: false
|
|
# The Gateway(s) this route attaches to - a platform object, usually in
|
|
# another namespace, which must allow this namespace in its listener's
|
|
# allowedRoutes. One entry per parent, the fields are the Gateway API's
|
|
# ParentReference (name, and optionally namespace, sectionName, port, group,
|
|
# kind), passed through unchanged:
|
|
#
|
|
# parentRefs:
|
|
# - name: public
|
|
# namespace: gateway
|
|
# sectionName: https
|
|
#
|
|
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
|
|
# list, because a route without a parent is created, is valid and routes
|
|
# nothing, with no event to say so.
|
|
parentRefs: []
|
|
# Empty means [ingress.domain], so the hostname has one source of truth. Set
|
|
# explicitly only to publish under a different or an additional name - which
|
|
# is how a migration from the Ingress runs: the HTTPRoute takes a second
|
|
# hostname while the Ingress keeps ingress.domain.
|
|
#
|
|
# For THIS chart, mind that a second hostname does not move PAPERLESS_URL
|
|
# with it: Django keeps trusting the origin built from ingress.domain, and
|
|
# every login POST from the new name is refused as a CSRF failure until that
|
|
# value is changed too (and the pods restarted).
|
|
hostnames: []
|
|
|
|
paperless:
|
|
# Replica count for this workload; 0 is allowed and scales it down.
|
|
# The chart-global scaleDown flag takes precedence.
|
|
replicas: 1
|
|
# Auxiliary busybox image, used by the /run-ownership init container and the
|
|
# SMB consume-mover CronJob. Pinned independently of appVersion (which
|
|
# tracks paperless-ngx); reviewed alongside every app-version update round.
|
|
busybox:
|
|
image:
|
|
repository: busybox
|
|
tag: "1.38.0"
|
|
ocr:
|
|
languages: "eng"
|
|
# Paperless-ngx 3.x modes: auto (default, skips OCR when embedded text
|
|
# exists), redo, force, off. The 2.x values "skip"/"skip_noarchive" were
|
|
# removed upstream; "skip" maps to "auto".
|
|
mode: "auto"
|
|
# Archive (PDF/A) generation, decoupled from OCR in 3.0: auto (default,
|
|
# skip archive for born-digital PDFs), always (2.x behavior), never.
|
|
archiveFileGeneration: "auto"
|
|
rotate_pages: "true"
|
|
output_type: "pdfa"
|
|
files:
|
|
filenameFormat: "{{ created_year }}/{{ correspondent }}/{{ title }}"
|
|
storage: 20Gi
|
|
maxUploadFileSize: 10m
|
|
# The barcode scanner backend is no longer configurable: paperless-ngx 3.0
|
|
# removed pyzbar and PAPERLESS_CONSUMER_BARCODE_SCANNER; zxing-cpp is the
|
|
# only backend now.
|
|
#
|
|
# An empty or null "barcodes:" block in an override means "the defaults
|
|
# below apply" — the chart merges the block onto these defaults instead of
|
|
# letting a childless key erase them (issue #61; the fallback copy lives in
|
|
# the paperless-ngx.barcodes helper and is kept in sync with this block).
|
|
# To switch barcode handling off, set barcodes.enabled: false explicitly;
|
|
# deleting the sub-keys does not disable it.
|
|
barcodes:
|
|
enabled: true
|
|
barcodeString: "PATCHT"
|
|
retainBarcodePage: false
|
|
asn:
|
|
enabled: true
|
|
# Paperless-ngx 3.0 no longer rejects duplicate documents by default; set
|
|
# to true to restore the 2.x behavior of deleting duplicates on consume.
|
|
deleteDuplicates: false
|
|
tikaGotenberg:
|
|
enabled: false
|
|
tikaEndpoint: null
|
|
gotenbergEndpoint: null
|
|
scripts:
|
|
postConsumptionScripts: []
|
|
preConsumptionScripts: []
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 8000
|
|
periodSeconds: 30
|
|
failureThreshold: 3
|
|
successThreshold: 1
|
|
timeoutSeconds: 5
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 8000
|
|
periodSeconds: 10
|
|
failureThreshold: 3
|
|
successThreshold: 1
|
|
timeoutSeconds: 5
|
|
# The startup probe gates liveness/readiness. Paperless runs database
|
|
# migrations on startup, and since 3.0 the first start after an upgrade
|
|
# also rebuilds the search index from scratch (Whoosh -> Tantivy):
|
|
# budget 10s * 60 = 600s.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 8000
|
|
periodSeconds: 10
|
|
failureThreshold: 60
|
|
timeoutSeconds: 5
|
|
# Extra environment variables, appended to the ones the chart always sets.
|
|
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
|
|
# forms are supported. paperless-ngx reads around 80 PAPERLESS_* variables and
|
|
# this chart maps a handful of them explicitly - everything else goes here.
|
|
#
|
|
# env:
|
|
# - name: PAPERLESS_LOGGING_DIR
|
|
# value: /tmp/paperless-log
|
|
#
|
|
# ADDITIVE MEANS ADDITIVE, and the timing matters: an entry repeating a name
|
|
# the chart already sets reaches the pod twice, and at INSTALL the later one -
|
|
# yours - wins. On an EXISTING release the same entry is lost, because
|
|
# Kubernetes merges env by name and the pair collapses to the chart's own
|
|
# value, while helm upgrade still reports success. Read an override back off
|
|
# the StatefulSet, never off the upgrade's exit code.
|
|
#
|
|
# VALUES MUST BE STRINGS (the schema enforces it) and they are quoted on the
|
|
# way out since 13.0.0. Before that a value beginning with "*" aborted the
|
|
# render and "true" or "0755" arrived as a bool or an integer, which the API
|
|
# server rejects - see the comment at the render site in
|
|
# templates/paperless-server.sfs.yaml.
|
|
#
|
|
# THE WORKED EXAMPLE, PAPERLESS_LOGGING_DIR, AND WHAT IT DOES NOT FIX
|
|
# (jk-cluster-config #360): with the data directory on an RWX volume that
|
|
# Longhorn exports over NFS, paperless' concurrent_log_handler cannot hold its
|
|
# lock file there and fails with "RuntimeError: Cannot acquire lock after 20
|
|
# attempts" - measured at 45 % of the application log across all three
|
|
# installations. Pointing PAPERLESS_LOGGING_DIR at a path under the /tmp
|
|
# emptyDir this pod already mounts takes the log off the NFS share and the
|
|
# noise stops. It does NOT fix the RWX data volume, which is the actual root
|
|
# cause and a cluster-config decision (the PVC asks for ReadWriteOnce and was
|
|
# provisioned ReadWriteMany; taking RWX back means recreating the volume).
|
|
# A log under /tmp does not survive a pod restart - no loss here, paperless
|
|
# also writes to stdout, which is what kubectl logs reads.
|
|
env: []
|
|
resources:
|
|
# Limits are optional: memory and ephemeral-storage limits default to
|
|
# limitFactor x the request, an explicitly set limit always wins, and a
|
|
# CPU limit is only set when configured explicitly under "limits".
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 0.2
|
|
memory: 1024Mi
|
|
ephemeralStorage: 200Mi
|
|
# Paperless-ngx supports rootless operation: the pod runs as uid/gid 1000
|
|
# (the image's default paperless user), fsGroup makes the PVCs writable and
|
|
# /tmp + /run are emptyDirs so the root filesystem stays read-only.
|
|
# Documented deviation: s6-overlay requires /run to be OWNED by the runtime
|
|
# uid (group-write is not enough), so a minimal root init container chowns
|
|
# the /run emptyDir to runAsUser/runAsGroup before the hardened main
|
|
# container starts.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: Always
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
smbConnection:
|
|
enabled: false
|
|
server: null
|
|
# Cron schedule of the consume mover. Quote it or not as you like - the chart
|
|
# quotes it on the way into the manifest (since 13.0.0, Gitea #11), which is
|
|
# what a schedule beginning with "*" or "@" needs. Before that such a
|
|
# schedule aborted the render with a YAML error pointing at the template.
|
|
cronJob: "0 0,12 * * *"
|
|
cron:
|
|
# Run the mover on the node that carries the StatefulSet pod (Gitea #101).
|
|
#
|
|
# WHY IT DEFAULTS TO TRUE: this pod mounts the consume volume, which is RWX
|
|
# and comes from Longhorn, and Longhorn's CSI driver refuses to mount an RWX
|
|
# volume unless it finds a workload pod in Pending - a mover pod that ran on
|
|
# a different node leaves the kubelet of the StatefulSet's node retrying
|
|
# every two minutes with "no Pending workload pods for volume ... to be
|
|
# mounted" (291 events in 10 hours, in both releases with SMB on; the
|
|
# release without the CronJob produced none).
|
|
#
|
|
# It is a switch and not a free-form affinity block on purpose: the default
|
|
# has to name the StatefulSet pod's label, that label contains the RELEASE
|
|
# NAME, and a values.yaml is never templated - so the useful default cannot
|
|
# be expressed here at all (the same property that forces
|
|
# valkey.secrets.authSecretRef to be typed out twice, further down).
|
|
#
|
|
# WHAT IT COSTS, AND IT IS A REAL COST: the affinity is required, so with
|
|
# the StatefulSet pod absent the mover cannot be scheduled. The two ways
|
|
# that happens are handled rather than left open - scaleDown: true suspends
|
|
# this CronJob outright, and activeDeadlineSeconds below fails a job that
|
|
# cannot be placed instead of letting Pending pods pile up. What stays: with
|
|
# paperless.replicas: 0 and SMB on, the mover does not run and reports one
|
|
# failed job per schedule. Set this to false to get the old, unpinned
|
|
# behaviour back - including the mount errors.
|
|
pinToServerNode: true
|
|
# How late a missed run may still start. Also keeps the CronJob controller
|
|
# from reaching "too many missed start times (> 100)", after which it stops
|
|
# scheduling this job entirely and says so only in its own log - reachable
|
|
# by leaving scaleDown: true in place for months. The cost of a window: a
|
|
# run that could not start inside it is skipped, not queued. Must stay well
|
|
# above 10s, below which the controller may never schedule at all.
|
|
startingDeadlineSeconds: 600
|
|
# Hard stop for one mover run, counted from the job's start - so it also
|
|
# bounds a pod that never gets scheduled (see pinToServerNode). Exceeding it
|
|
# shows up as a job with reason DeadlineExceeded, which is the point: a
|
|
# loud, self-clearing failure instead of a Pending pod that nobody sees.
|
|
# Raise it if a run legitimately moves more than an hour's worth of files;
|
|
# the mover copies across filesystems, so large scans are not instant.
|
|
activeDeadlineSeconds: 3600
|
|
# Extra environment variables for the mover container, same form as
|
|
# paperless.env above. Deliberately empty: the container is a busybox
|
|
# running one mv, so this exists for the odd proxy or locale variable and
|
|
# for the convention that every workload container offers the block - not
|
|
# because the mover needs configuration. Note that the cron SCHEDULE's time
|
|
# zone is the controller's and cannot be changed with a TZ entry here.
|
|
env: []
|
|
# The consume mover only shuffles files between the SMB share (mounted
|
|
# with 0777 modes) and the consume PVC, so it runs as the same non-root
|
|
# uid/gid as paperless itself with a fully hardened context.
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: Always
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
resources:
|
|
# Limits are optional: memory and ephemeral-storage limits default to
|
|
# limitFactor x the request, an explicitly set limit always wins, and a
|
|
# CPU limit is only set when configured explicitly under "limits".
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 10m
|
|
memory: 16Mi
|
|
ephemeralStorage: 10Mi
|
|
oidc:
|
|
enabled: false
|
|
autoLoginWithProvider: false
|
|
disableRegularLogin: false
|
|
provider:
|
|
id: null
|
|
name: null
|
|
clientId: null
|
|
discoveryUrl: null
|
|
# Optional: explicit token endpoint auth method. "none" is the only accepted
|
|
# value, and the schema enforces that (Gitea #11).
|
|
#
|
|
# WHY IT IS RESTRICTED, since the previous comment here named
|
|
# client_secret_basic as the example and that was actively misleading: this
|
|
# chart registers a PUBLIC client and configures NO client secret - it
|
|
# renders OAUTH_PKCE_ENABLED and nothing else, and secrets.oidcSecretRef was
|
|
# removed in 13.0.0 because no secret can reach paperless through a json
|
|
# environment variable anyway. A method that authenticates WITH a client
|
|
# secret therefore cannot work here; asking for one produced a login that
|
|
# died at the callback with invalid_client, while the two values read
|
|
# together looked like a working secret setup.
|
|
#
|
|
# So: null omits the key and lets allauth decide, "none" states the public
|
|
# client explicitly - which is what some providers need spelled out since
|
|
# the allauth update in paperless-ngx 3.0, and the symptom when they do is
|
|
# that same invalid_client at the callback. If a future paperless release
|
|
# supports an auth method that needs no secret, widening the enum is a minor
|
|
# bump; allowing one that needs a secret is not on the table.
|
|
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.
|