{{- 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: {{- /* 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" $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" $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 - if [ "$(find /mnt/smb -maxdepth 1 -name "*.pdf" -print)" ]; then mv /mnt/smb/*.pdf /mnt/consume; fi volumeMounts: - name: smb-share mountPath: "/mnt/smb" readOnly: false - name: consume-folder mountPath: "/mnt/consume" readOnly: false resources: {{- include "paperless-ngx.cron.effectiveResources" $cron.resources | nindent 16 }} restartPolicy: Never volumes: - name: smb-share persistentVolumeClaim: claimName: {{ .Release.Name }}-{{ .Chart.Name }}-smb-volume-claim - name: consume-folder persistentVolumeClaim: claimName: {{ .Release.Name }}-{{ .Chart.Name }}-consume-volume-claim {{- end }}