feat(paperless-ngx)!: quote every operator scalar, pin the consume mover, drop oidcSecretRef #131

Merged
jklein merged 2 commits from feat/paperless-ngx-scalars-pinning-oidc into main 2026-10-13 21:29:32 +02:00
8 changed files with 427 additions and 42 deletions
Showing only changes of commit 4b2bde505d - Show all commits

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/dev/src-ui/s
type: application
version: 12.3.0+up3.2.1
version: 13.0.0+up3.2.1
appVersion: 3.2.1
+70 -17
View File
@@ -8,6 +8,9 @@
{{- $valkeySvc := printf "%s-valkey-service" .Release.Name -}}
{{- $pre := default (list) $scripts.preConsumptionScripts -}}
{{- $post := default (list) $scripts.postConsumptionScripts -}}
{{- $cron := fromYaml (include "paperless-ngx.cron.scheduling" .Values.smbConnection.cron) -}}
{{- $cronPinned := $cron.pinToServerNode -}}
{{- $cronDeadline := include "paperless-ngx.formatNumber" $cron.activeDeadlineSeconds -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
@@ -184,15 +187,14 @@ WHAT THE 1PASSWORD ITEMS MUST CONTAIN
here, so a wrong field name surfaces as a mount failure on the CronJob's
pod — a describe on that pod, never a missing variable.
{{- end }}
{{- if .Values.secrets.oidcSecretRef }}
secrets.oidcSecretRef IS SET AND IS READ BY NOTHING
"{{ .Values.secrets.oidcSecretRef }}" — no template in this chart references
that value. No OnePasswordItem is created for it and no Secret appears. The
OIDC client id travels as plain env from oidc.provider.clientId, and no
client secret is configured at all (see the OIDC section below). Whatever
you put in that item has no effect on this release.
{{- end }}
{{- /*
The warning that used to stand here - "secrets.oidcSecretRef IS SET AND IS READ
BY NOTHING" - is gone with the value itself (13.0.0, Gitea #11). It cannot be
printed any more and would not be reached: the key is out of values.schema.json
too, so a release that still sets it fails BEFORE any template renders, with
"additional properties 'oidcSecretRef' not allowed". The fix is to delete the
key; values.yaml says why it is not coming back.
*/}}
CHANGING A SECRET'S CONTENT RESTARTS NOTHING
Helm never sees the content of a OnePasswordItem, so it cannot hash it, and
@@ -340,12 +342,44 @@ HOW DOCUMENTS GET IN
move, not a copy: once the job has run, the share no longer holds the file
and the only copy is on the consume volume.
One thing to know before you edit that schedule: it is rendered into the
manifest unquoted, so a schedule that STARTS with "*" ("*/15 * * * *")
aborts the render with a YAML parse error on this template — YAML reads the
leading * as an alias. Quoting it in your values file does not help, the
quotes are gone by then. Use an equivalent that starts with a digit
("5,20,35,50 * * * *"); a "*" anywhere but in first position is fine.
The schedule is quoted on the way into the manifest since 13.0.0, so any
cron notation works — one starting with "*" or an "@daily" macro included.
Before that such a schedule aborted the render with a YAML parse error
pointing at the template, and the advice here was to avoid it; if you are
carrying a workaround schedule from that era ("5,20,35,50 * * * *" instead
of every 15 minutes), you can write what you meant now.
{{- if $cronPinned }}
PLACEMENT: the mover runs on the NODE OF THE STATEFULSET POD, via a
required podAffinity on its label (Gitea #101). Without that it lands
anywhere, and because it mounts the consume volume — RWX, from Longhorn —
the kubelet of the StatefulSet's node then retries the mount every two
minutes with "no Pending workload pods for volume ... to be mounted". It
was measured at 291 such events in 10 hours.
THE PRICE, and it is the one thing to know before scaling down: with no
StatefulSet pod the mover CANNOT BE SCHEDULED. scaleDown: true suspends
this CronJob outright, so that case is covered — but paperless.replicas: 0
with SMB still on is not, and there the job fails once per schedule after
activeDeadlineSeconds ({{ $cronDeadline }}s) with reason DeadlineExceeded. That is
deliberate: a bounded, visible failure rather than a Pending pod per
schedule that nobody sees. Switch smbConnection.cron.pinToServerNode to
false to go back to unpinned placement — and to the mount errors.
What the pinning does NOT fix: the consume volume stays RWX with a
share-manager behind it. Co-locating the two consumers removes the
cross-node mount, not the second consumer.
{{- else }}
PLACEMENT IS OFF (smbConnection.cron.pinToServerNode: false). The mover
will be scheduled wherever there is room, and when that is not the node of
the StatefulSet pod, Longhorn refuses to mount the shared RWX consume
volume there: the kubelet of the StatefulSet's node retries every two
minutes with "no Pending workload pods for volume ... to be mounted"
(Gitea #101, measured at 291 events in 10 hours). The errors stop when the
StatefulSet pod is deleted and comes back Pending, and return with the next
firing of this job.
{{- end }}
The job has NO probes (the documented exception to the chart conventions),
and it cannot fail usefully either: with nothing to move it exits 0, which
@@ -432,8 +466,13 @@ OIDC IS ON, AND IT IS PKCE-ONLY
{{- if not .Values.oidc.provider.tokenAuthMethod }}
oidc.provider.tokenAuthMethod is unset. Since the allauth update in 3.0 some
providers need it spelled out (client_secret_basic); the symptom is a login
that dies at the callback with invalid_client.
providers need it spelled out, and the symptom when they do is a login that
dies at the callback with invalid_client. The only value this chart accepts is
"none" — it states the public client explicitly. A method that authenticates
WITH a client secret cannot work here and is rejected by the schema (13.0.0,
Gitea #11): this release configures no client secret and cannot, because
paperless reads the whole OIDC configuration out of one json environment
variable with no _FILE variant.
{{- end }}
{{- if or .Values.oidc.autoLoginWithProvider .Values.oidc.disableRegularLogin }}
@@ -453,6 +492,20 @@ paperless.scripts IS SET AND DOES NOTHING
PAPERLESS_PRE_CONSUME_SCRIPT nor PAPERLESS_POST_CONSUME_SCRIPT is set on the
container. These values render no object and change no behaviour.
{{- end }}
{{- if .Values.paperless.env }}
paperless.env ADDS {{ len .Values.paperless.env }} VARIABLE(S) — AND AN OVERRIDE ONLY BITES AT INSTALL
{{ range $i, $e := .Values.paperless.env }}{{ if $i }}, {{ end }}{{ $e.name }}{{ end }}
These are appended AFTER the variables the chart sets itself, so at INSTALL an
entry repeating one of those names wins. On an upgrade of an EXISTING release
the same entry is lost: Kubernetes merges env by name, the pair collapses to
the chart's own value, and helm upgrade still reports success. Read the value
back off the StatefulSet, never off the upgrade's exit code:
kubectl -n {{ .Release.Namespace }} get statefulset {{ .Release.Name }}-{{ .Chart.Name }}-sfs \
-o jsonpath='{.spec.template.spec.containers[0].env}'
{{- end }}
{{- if gt (int $replicas) 1 }}
paperless.replicas IS {{ $replicas }} — EXPECT ONE POD TO WORK AND THE REST TO WAIT
+38
View File
@@ -340,3 +340,41 @@ The same three fallbacks for the SMB consume-mover CronJob. It has no probes
{{- $merged := fromYaml (include "paperless-ngx.defaultedDict" (dict "defaults" $defaults "given" . "path" "smbConnection.cron.resources")) -}}
{{- include "paperless-ngx.resources" $merged -}}
{{- end -}}
{{/*
The whole smbConnection.cron block, merged onto its defaults (YAML; consume
with fromYaml).
Input: .Values.smbConnection.cron (may be nil, empty or partial).
WHY EVERY READ OF THAT BLOCK GOES THROUGH HERE. The CronJob template used to
dereference .Values.smbConnection.cron.<key> directly, so an override that had
erased the block - the leftover childless
cron:
that issue #61 is about - did not fall back to the defaults the way this chart
promises everywhere else. It aborted the render with
nil pointer evaluating interface {}.securityContext
naming an internal template path and neither the value nor the cause. Measured
on origin/main before 13.0.0, so this is a fix and not a precaution.
The scalar defaults below MUST stay in sync with the smbConnection.cron block
in values.yaml, which stays the documented, user-facing place for them; this
copy is only the fallback. The sub-blocks (securityContext, resources, env)
keep their own defaults in the helpers above and are only passed through here.
An explicit "pinToServerNode: false" survives this, which is the whole reason
it is not a "default true" pipe: sprig's default would read false as unset and
silently turn the pinning back on - the one value whose explicit false has to
be obeyed, since it exists to switch off a hard placement rule.
*/}}
{{- define "paperless-ngx.cron.scheduling" -}}
{{- $defaults := dict
"pinToServerNode" true
"startingDeadlineSeconds" 600
"activeDeadlineSeconds" 3600 -}}
{{- include "paperless-ngx.defaultedDict" (dict "defaults" $defaults "given" . "path" "smbConnection.cron") -}}
{{- end -}}
@@ -1,26 +1,165 @@
{{- if .Values.smbConnection.enabled }}
{{- /*
The cron block, merged onto its defaults, so that an override which erased it
falls back instead of aborting with a nil-pointer error naming an internal
template path - which is what every direct .Values.smbConnection.cron.<key>
read did before 13.0.0. The helper carries the measurement.
*/}}
{{- $cron := fromYaml (include "paperless-ngx.cron.scheduling" .Values.smbConnection.cron) -}}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-consume-cronjob
spec:
schedule: {{ .Values.smbConnection.cronJob }}
{{- /*
QUOTED, AND THAT IS THE WHOLE OF GITEA #11's FIRST HALF. The default
"0 0,12 * * *" renders safely only because it begins with a digit. The most
common cron notation of all - a star, a slash and an interval, every 15
minutes - begins with "*", which YAML reads as an alias indicator, and the
render ABORTS:
(and the notation cannot be written out in this comment, because the two
characters that start it would end a Go template comment right here)
Error: YAML parse error on paperless-ngx/templates/paperless-consume.cronjob.yaml:
error converting YAML to JSON: yaml: line 6: did not find expected
alphabetic or numeric character
The trap is that quoting it in the VALUES does not help: Helm passes the
string's content through, and the star stands bare in the manifest again. So
the operator writes a correctly quoted value and gets an error that points at
this template instead of at their value. "@daily" fails the same way ("@" is
a reserved YAML indicator).
ci/run-yaml-scalar-render-test.sh is the test that would have caught it, and
ci/yaml-scalar/paperless-ngx.yaml is the overlay that feeds it a star-leading
schedule - needed because this whole template sits behind
smbConnection.enabled, which is false in every other fixture, so nothing else
in the repository renders this object at all.
*/}}
schedule: {{ .Values.smbConnection.cronJob | quote }}
{{- /*
Forbid, hard-wired rather than a value: two movers running at once race on
the same files ("mv /mnt/smb/*.pdf" twice over one directory), and the loser
reports a file that is already gone. There is no configuration in which
overlapping runs of this job are wanted, so there is nothing to configure.
*/}}
concurrencyPolicy: Forbid
{{- /*
Bounds how late a missed run may still start, and it is here for a second
reason that has nothing to do with lateness: while the CronJob is suspended
(scaleDown) or its runs are being suppressed, the controller counts missed
schedules, and above 100 it STOPS SCHEDULING ALTOGETHER with only
"Cannot determine if job needs to be started: too many missed start times" in
the controller log. With this field set, the controller only counts misses
inside this window, so that state cannot be reached. The cost is stated in
values.yaml: a run that could not start inside the window is skipped, not
queued.
*/}}
startingDeadlineSeconds: {{ include "paperless-ngx.formatNumber" $cron.startingDeadlineSeconds }}
{{- if .Values.scaleDown }}
suspend: true
{{- end }}
jobTemplate:
spec:
{{- /*
THE SAFETY NET UNDER THE AFFINITY BELOW (Gitea #101). activeDeadlineSeconds
counts from the job's start, not from the pod's, so it also bounds a pod
that NEVER gets scheduled: when the StatefulSet pod is absent and the
required affinity cannot be satisfied, the job fails after this many
seconds with reason DeadlineExceeded instead of sitting Pending forever
and gaining a sibling every schedule. That turns the one new failure mode
this issue's fix could introduce into a visible, self-clearing one -
failedJobsHistoryLimit (Kubernetes' default, 1) keeps the evidence.
*/}}
activeDeadlineSeconds: {{ include "paperless-ngx.formatNumber" $cron.activeDeadlineSeconds }}
template:
spec:
{{- if $cron.pinToServerNode }}
{{- /*
GITEA #101: this pod mounts the consume volume, which is RWX and comes
from Longhorn. Without a placement rule the scheduler puts it on any
node, and Longhorn's CSI driver then refuses the mount on the node
where the StatefulSet pod runs:
MountVolume.SetUp failed ... code = Aborted desc = no Pending
workload pods for volume pvc-... to be mounted:
map[Running:[...-sfs-0] Succeeded:[...-consume-cronjob-...]]
Measured at every two minutes, 291 events in 10 hours, in both
releases that have SMB switched on; the release with
smbConnection.enabled: false and therefore no CronJob produced none of
them on identically built volumes.
WHY podAffinity AND NOT nodeSelector: the StatefulSet pod is not
pinned either - it sits wherever the scheduler put it and moves on
every restart. A static nodeSelector would be correct exactly until
then and silently wrong afterwards. The affinity follows the pod.
WHY required AND NOT preferred: preferred falls back to another node
in the very situation this exists for, which reproduces the bug more
rarely and therefore harder to find. The fallback is what went
unnoticed for ten hours.
The label is the one the StatefulSet puts on its pod template
(paperless-server.sfs.yaml, metadata.labels.app) and is read off the
running pod, not assumed.
WHAT THIS DOES NOT FIX: the volume stays RWX and keeps its
share-manager; co-locating the two consumers removes the CROSS-NODE
attach, not the second consumer. The design that removes it is moving
the mv into the StatefulSet pod, which cannot be done for the running
installations because RWX cannot be taken back off an existing PVC.
Gitea #101 names it as the alternative and not as the proposal.
*/}}
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
topologyKey: kubernetes.io/hostname
{{- end }}
automountServiceAccountToken: false
securityContext:
{{- include "paperless-ngx.cron.podSecurityContext" .Values.smbConnection.cron.securityContext | nindent 12 }}
{{- include "paperless-ngx.cron.podSecurityContext" $cron.securityContext | nindent 12 }}
containers:
- name: paperless-consume-cronjob
image: "{{ .Values.paperless.busybox.image.repository }}:{{ .Values.paperless.busybox.image.tag }}"
imagePullPolicy: IfNotPresent
securityContext:
{{- include "paperless-ngx.cron.containerSecurityContext" .Values.smbConnection.cron.securityContext | nindent 16 }}
{{- include "paperless-ngx.cron.containerSecurityContext" $cron.securityContext | nindent 16 }}
{{- if $cron.env }}
env:
{{- range $value := $cron.env }}
{{- if $value.value }}
{{- /*
quote is required, not cosmetic: a value like "8080" or "true"
renders unquoted as a YAML number/bool, and the API server then
rejects the whole manifest with "cannot unmarshal number into
Go struct field EnvVar.value of type string". A template
comment and not a "#" one, because a "#" line here is part of
the rendered manifest.
*/}}
- name: {{ $value.name | quote }}
value: {{ tpl $value.value $ | quote }}
{{- else if $value.valueFrom }}
{{- if $value.valueFrom.secretKeyRef }}
- name: {{ $value.name }}
valueFrom:
secretKeyRef:
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
{{- else if $value.valueFrom.configMapKeyRef }}
- name: {{ $value.name }}
valueFrom:
configMapKeyRef:
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
command:
- /bin/sh
- -c
@@ -33,7 +172,7 @@ spec:
mountPath: "/mnt/consume"
readOnly: false
resources:
{{- include "paperless-ngx.cron.effectiveResources" .Values.smbConnection.cron.resources | nindent 16 }}
{{- include "paperless-ngx.cron.effectiveResources" $cron.resources | nindent 16 }}
restartPolicy: Never
volumes:
- name: smb-share
@@ -42,4 +181,4 @@ spec:
- name: consume-folder
persistentVolumeClaim:
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-consume-volume-claim
{{- end }}
{{- end }}
@@ -204,23 +204,41 @@ spec:
- name: PAPERLESS_DISABLE_REGULAR_LOGIN
value: "{{ .Values.oidc.disableRegularLogin }}"
{{- end }}
{{- /*
QUOTED SINCE 13.0.0, and it is a fix rather than a tidy-up (Gitea
#112, same genus as #11). This block is the chart's additive env
interface, and until 13.0.0 it rendered its operand bare. Two
measured consequences, both of them the operator's value arriving
as something else:
- a value beginning with "*" aborted the render, with the error
pointing at THIS template rather than at the value;
- "true" and "0755" rendered as a YAML bool and an integer, and
the API server rejects those with "cannot unmarshal number
into Go struct field EnvVar.value of type string" - the chart
renders, the install fails.
The quoted form is the repository's documented one
(template-chart/templates/xxx.deployment.yaml carries the rule);
this chart was one of seven that had not been brought to it.
*/}}
{{- range $value := .Values.paperless.env }}
{{- if $value.value }}
- name: {{ $value.name }}
value: {{ tpl $value.value $ }}
- name: {{ $value.name | quote }}
value: {{ tpl $value.value $ | quote }}
{{- else if $value.valueFrom }}
{{- if $value.valueFrom.secretKeyRef }}
- name: {{ $value.name }}
valueFrom:
secretKeyRef:
name: {{ tpl $value.valueFrom.secretKeyRef.name $ }}
key: {{ tpl $value.valueFrom.secretKeyRef.key $ }}
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
{{- else if $value.valueFrom.configMapKeyRef }}
- name: {{ $value.name }}
valueFrom:
configMapKeyRef:
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ }}
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ }}
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
{{- end }}
{{- end }}
{{- end }}
+10 -2
View File
@@ -23,8 +23,16 @@ metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
annotations:
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.paperless.files.maxUploadFileSize }}
nginx.org/client-max-body-size: {{ .Values.paperless.files.maxUploadFileSize }}
{{- /*
Quoted for the same reason as the env block in the StatefulSet and the
schedule in the CronJob (Gitea #11's genus): an annotation value must be a
string, maxUploadFileSize is free operator text, and a unit-less "100" -
schema-valid, since the schema types it as a string - renders bare as an
integer and the API server rejects the whole Ingress. The default "10m"
happens to be safe, which is exactly how this survived.
*/}}
nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.paperless.files.maxUploadFileSize | quote }}
nginx.org/client-max-body-size: {{ .Values.paperless.files.maxUploadFileSize | quote }}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
+19 -6
View File
@@ -69,9 +69,6 @@
},
"smbConnectionSecretRef": {
"type": ["string", "null"]
},
"oidcSecretRef": {
"type": ["string", "null"]
}
}
},
@@ -279,13 +276,28 @@
"type": ["string", "null"]
},
"cronJob": {
"description": "Cron schedule of the consume-mover job.",
"description": "Cron schedule of the consume-mover job. Deliberately without a pattern: cron syntax is richer than a regex here would admit, and the failure this value used to have was not a wrong schedule but a correct one rendered bare into the manifest - fixed at the render site with | quote (Gitea #11), not by narrowing what may be written.",
"type": ["string", "null"]
},
"cron": {
"type": "object",
"additionalProperties": false,
"properties": {
"pinToServerNode": {
"description": "Place the consume-mover pod on the node that carries the StatefulSet pod, via a required podAffinity on its label (Gitea #101). Default true: the mover 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 leaves the StatefulSet node's kubelet retrying every two minutes. false restores the unpinned behaviour and with it those mount errors.",
"type": ["boolean", "null"]
},
"startingDeadlineSeconds": {
"description": "How late a missed run may still start. Also bounds the CronJob controller's missed-schedule count, which above 100 stops this job being scheduled at all, reported only in the controller's own log.",
"type": ["integer", "null"]
},
"activeDeadlineSeconds": {
"description": "Hard stop for one mover run, counted from the job's start, so it also bounds a pod that never gets scheduled - with pinToServerNode on and no StatefulSet pod, the job fails with DeadlineExceeded instead of leaving a Pending pod per schedule.",
"type": ["integer", "null"]
},
"env": {
"$ref": "#/definitions/env"
},
"securityContext": {
"$ref": "#/definitions/securityContext"
},
@@ -326,8 +338,9 @@
"type": ["string", "null"]
},
"tokenAuthMethod": {
"description": "Explicit token endpoint auth method (e.g. client_secret_basic). Some providers need this since the allauth update in paperless-ngx 3.0.",
"type": ["string", "null"]
"description": "Explicit token endpoint auth method. Restricted to 'none' in 13.0.0 (Gitea #11): this chart registers a PUBLIC client with PKCE and configures no client secret, so a method that authenticates with one cannot work - it produced a login that died at the callback with invalid_client while reading like a working secret setup. null omits the key; 'none' states the public client explicitly, which some providers need spelled out since the allauth update in paperless-ngx 3.0.",
"type": ["string", "null"],
"enum": ["none", null]
}
}
}
+121 -5
View File
@@ -13,7 +13,26 @@ secrets:
databaseSecretRef: null
paperlessSecretRef: null
smbConnectionSecretRef: null
oidcSecretRef: 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
@@ -179,7 +198,38 @@ paperless:
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.
# 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
@@ -214,8 +264,58 @@ paperless:
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.
@@ -251,9 +351,25 @@ oidc:
name: null
clientId: null
discoveryUrl: null
# Optional: explicit token endpoint auth method (e.g. client_secret_basic).
# Some providers need this since the allauth update in paperless-ngx 3.0
# (login fails at the callback with invalid_client otherwise).
# 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
# ---------------------------------------------------------------------------