diff --git a/paperless-ngx/Chart.yaml b/paperless-ngx/Chart.yaml index 8c4b3cd..957c2c2 100644 --- a/paperless-ngx/Chart.yaml +++ b/paperless-ngx/Chart.yaml @@ -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 diff --git a/paperless-ngx/templates/NOTES.txt b/paperless-ngx/templates/NOTES.txt index 268c484..68defad 100644 --- a/paperless-ngx/templates/NOTES.txt +++ b/paperless-ngx/templates/NOTES.txt @@ -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 diff --git a/paperless-ngx/templates/_helpers.tpl b/paperless-ngx/templates/_helpers.tpl index 5083293..2fba559 100644 --- a/paperless-ngx/templates/_helpers.tpl +++ b/paperless-ngx/templates/_helpers.tpl @@ -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. 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 -}} diff --git a/paperless-ngx/templates/paperless-consume.cronjob.yaml b/paperless-ngx/templates/paperless-consume.cronjob.yaml index ba8ba82..db2c7bb 100644 --- a/paperless-ngx/templates/paperless-consume.cronjob.yaml +++ b/paperless-ngx/templates/paperless-consume.cronjob.yaml @@ -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. +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 }} \ No newline at end of file +{{- end }} diff --git a/paperless-ngx/templates/paperless-server.sfs.yaml b/paperless-ngx/templates/paperless-server.sfs.yaml index 1484a87..6a10397 100644 --- a/paperless-ngx/templates/paperless-server.sfs.yaml +++ b/paperless-ngx/templates/paperless-server.sfs.yaml @@ -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 }} diff --git a/paperless-ngx/templates/paperless.ingress.yaml b/paperless-ngx/templates/paperless.ingress.yaml index 33f6287..1270324 100644 --- a/paperless-ngx/templates/paperless.ingress.yaml +++ b/paperless-ngx/templates/paperless.ingress.yaml @@ -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 }} diff --git a/paperless-ngx/values.schema.json b/paperless-ngx/values.schema.json index d5d2758..dffd273 100644 --- a/paperless-ngx/values.schema.json +++ b/paperless-ngx/values.schema.json @@ -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] } } } diff --git a/paperless-ngx/values.yaml b/paperless-ngx/values.yaml index 431d6df..9282567 100644 --- a/paperless-ngx/values.yaml +++ b/paperless-ngx/values.yaml @@ -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 # ---------------------------------------------------------------------------