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
468 lines
21 KiB
JSON
468 lines
21 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
"title": "paperless-ngx values",
|
|
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key that the chart does not read is rejected at render time instead of being silently ignored. The only objects that stay open are the ones passed verbatim into the Kubernetes pod spec (probes, security contexts, global) - the chart does not interpret their contents, so it cannot judge which keys are meaningful. The hardening, probe and resources blocks of BOTH workloads, and the barcodes block from issue #61, additionally accept null, because an erased block falls back to the chart defaults (issues #61 and #99).",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["valkeySubchart"],
|
|
"allOf": [
|
|
{
|
|
"title": "With the valkey subchart switched on, valkey.secrets.authSecretRef is required",
|
|
"description": "The one cross-key rule in this file, and it lives here rather than in a template on purpose: Helm validates the schema BEFORE it renders anything, and it renders a subchart's templates before the parent's. A fail() in this chart's templates therefore loses the race against the subchart's own required(), whose message names 'secrets.authSecretRef' and cannot know that the path belongs to the same 1Password item as secrets.redisSecretRef. Measured for Gitea #78 - a guard that cannot fire is the mistake found while testing #77. Set it to the SAME op:// path as secrets.redisSecretRef: the bundled Valkey reads its password from the field 'auth-conf' in that item, whose value must be the complete line 'requirepass <password>'. A different item is allowed; two items then have to be kept in step.",
|
|
"if": {
|
|
"required": ["valkeySubchart"],
|
|
"properties": {
|
|
"valkeySubchart": {
|
|
"type": "object",
|
|
"required": ["enabled"],
|
|
"properties": {
|
|
"enabled": {
|
|
"const": true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"then": {
|
|
"required": ["valkey"],
|
|
"properties": {
|
|
"valkey": {
|
|
"type": "object",
|
|
"required": ["secrets"],
|
|
"properties": {
|
|
"secrets": {
|
|
"type": "object",
|
|
"required": ["authSecretRef"],
|
|
"properties": {
|
|
"authSecretRef": {
|
|
"type": "string",
|
|
"minLength": 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"properties": {
|
|
"global": {
|
|
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
|
"type": "object"
|
|
},
|
|
"scaleDown": {
|
|
"type": "boolean"
|
|
},
|
|
"secrets": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"redisSecretRef": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"databaseSecretRef": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"paperlessSecretRef": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"smbConnectionSecretRef": {
|
|
"type": ["string", "null"]
|
|
}
|
|
}
|
|
},
|
|
"ingress": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"description": "Gates the whole Ingress template. Added with the Gateway-API mode (Gitea #114), defaulting to true - this chart used to render its Ingress unconditionally, which left no second value path for the mutual-exclusion abort to name and kept the required() on clusterIssuer demanding a value the Gateway mode does not use. Mutually exclusive with httpRoute.enabled - the render aborts when both are true, and since this defaults to true it has to be switched off explicitly. false makes clusterIssuer unnecessary but NOT domain, which is also PAPERLESS_URL and carries its own required() in the StatefulSet.",
|
|
"type": "boolean"
|
|
},
|
|
"clusterIssuer": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"domain": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"className": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"annotations": {
|
|
"description": "Extra Ingress annotations, merged with the cert-manager and upload-size annotations the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead - including the upload cap the chart itself sets from paperless.files.maxUploadFileSize.",
|
|
"type": "object"
|
|
}
|
|
}
|
|
},
|
|
"httpRoute": {
|
|
"description": "Gateway-API alternative to the Ingress (Gitea #114). Mutually exclusive with ingress.enabled - the render aborts when both are true, and ingress.enabled defaults to true in this chart. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them. paperless.files.maxUploadFileSize is not enforced in this mode.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"parentRefs": {
|
|
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
|
|
"type": "array",
|
|
"items": {
|
|
"$ref": "#/definitions/parentRef"
|
|
}
|
|
},
|
|
"hostnames": {
|
|
"description": "Empty means [ingress.domain], so the hostname has a single source of truth. A hostname set here does not move PAPERLESS_URL, which keeps following ingress.domain - so Django refuses every login POST from the new name as a CSRF failure until both are set.",
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"paperless": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"replicas": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"busybox": {
|
|
"description": "Auxiliary image for the /run-ownership init container and the SMB consume-mover CronJob. Pinned independently of appVersion.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"image": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"repository": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"tag": {
|
|
"type": ["string", "number", "null"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"ocr": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"languages": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"mode": {
|
|
"description": "paperless-ngx 3.x modes. The 2.x values skip/skip_noarchive were removed upstream; skip maps to auto.",
|
|
"type": ["string", "null"],
|
|
"enum": ["auto", "redo", "force", "off", null]
|
|
},
|
|
"archiveFileGeneration": {
|
|
"description": "Archive (PDF/A) generation, decoupled from OCR in 3.0.",
|
|
"type": ["string", "null"],
|
|
"enum": ["auto", "always", "never", null]
|
|
},
|
|
"rotate_pages": {
|
|
"type": ["string", "boolean", "null"]
|
|
},
|
|
"output_type": {
|
|
"type": ["string", "null"]
|
|
}
|
|
}
|
|
},
|
|
"files": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"filenameFormat": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"storage": {
|
|
"description": "Size of the media PVC. Not null-safe on purpose: an erased value yields an invalid PVC rather than a silently degraded workload.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"maxUploadFileSize": {
|
|
"description": "Rendered into the ingress-nginx proxy-body-size and nginx.org/client-max-body-size annotations, so it caps web-UI uploads at the ingress controller and nowhere inside paperless. It does nothing with ingress.enabled: false, and it is NOT ENFORCED in Gateway mode either (Gitea #114): the Gateway API has no body-size field, the effective ceiling is the Gateway's, and this chart deliberately renders no implementation-specific policy object to set it. The consume directory is unaffected in every mode - it passes no proxy and has no limit.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"barcodes": {
|
|
"description": "May be null / empty: the chart then falls back to these defaults instead of letting a childless key erase them (issue #61). To switch barcode handling off, set enabled: false explicitly.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"barcodeString": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"retainBarcodePage": {
|
|
"type": "boolean"
|
|
},
|
|
"asn": {
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"deleteDuplicates": {
|
|
"description": "paperless-ngx 3.0 no longer rejects duplicates by default; true restores the 2.x behaviour.",
|
|
"type": "boolean"
|
|
},
|
|
"tikaGotenberg": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"description": "Boolean or string - all three production bundles write it as the string \"true\", and the value is passed straight through as PAPERLESS_TIKA_ENABLED. Note that the template gates on truthiness, so the STRING \"false\" still renders the block (and keeps the two endpoints required) even though the env value it emits is \"false\"; use a real boolean false to switch the block off.",
|
|
"type": ["boolean", "string", "null"]
|
|
},
|
|
"tikaEndpoint": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"gotenbergEndpoint": {
|
|
"type": ["string", "null"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"scripts": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"postConsumptionScripts": {
|
|
"type": ["array", "null"]
|
|
},
|
|
"preConsumptionScripts": {
|
|
"type": ["array", "null"]
|
|
}
|
|
}
|
|
},
|
|
"livenessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"readinessProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"startupProbe": {
|
|
"$ref": "#/definitions/probe"
|
|
},
|
|
"env": {
|
|
"$ref": "#/definitions/env"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"securityContext": {
|
|
"$ref": "#/definitions/securityContext"
|
|
}
|
|
}
|
|
},
|
|
"smbConnection": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"server": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"cronJob": {
|
|
"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"
|
|
},
|
|
"resources": {
|
|
"$ref": "#/definitions/resources"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"oidc": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
},
|
|
"autoLoginWithProvider": {
|
|
"type": "boolean"
|
|
},
|
|
"disableRegularLogin": {
|
|
"type": "boolean"
|
|
},
|
|
"provider": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"id": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"name": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"clientId": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"discoveryUrl": {
|
|
"type": ["string", "null"]
|
|
},
|
|
"tokenAuthMethod": {
|
|
"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]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"valkeySubchart": {
|
|
"description": "The dependency condition for the valkey subchart. A key of THIS chart, deliberately outside the 'valkey' subtree: everything under 'valkey:' is handed to the subchart as its values, and this repo's valkey chart has additionalProperties: false with no 'enabled' property, so 'valkey.enabled' is rejected by the subchart itself. Helm skips a disabled subchart's schema validation, so that error would only ever have surfaced for whoever first set it to true. NOT null-safe and 'enabled' is REQUIRED, which is the opposite of this chart's other blocks (issue #99) and is deliberate: Helm treats a condition whose path does not resolve as ENABLED, so an erased 'valkeySubchart:' would switch the subchart ON - measured for Gitea #78, where it surfaced as the subchart's own required() error. An erased block must not flip behaviour, least of all in that direction, so it is rejected here instead.",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["enabled"],
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean"
|
|
}
|
|
}
|
|
},
|
|
"valkey": {
|
|
"description": "Values passed through to the valkey subchart - this repo's OWN valkey chart, resolved from the published index. Open on purpose and at every level: the subchart owns this schema (it ships its own, and a stricter one than this), and a closed copy here would reject whatever that chart adds in its next release while duplicating a validation that already happens. This chart reads NONE of these keys; the dependency condition is valkeySubchart.enabled instead. Two shapes that are easy to get wrong: secrets.authSecretRef must be set BY HAND to the same op:// path as secrets.redisSecretRef (a values.yaml is never templated, so a parent cannot default it - Gitea #78), and the item's field 'auth-conf' holds the complete line 'requirepass <password>', not a bare password. values.yaml argues the queue-vs-cache choice and names the three operator traps.",
|
|
"type": "object"
|
|
}
|
|
},
|
|
"definitions": {
|
|
"parentRef": {
|
|
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
|
|
"type": "object",
|
|
"required": ["name"]
|
|
},
|
|
"resources": {
|
|
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"limitFactor": {
|
|
"type": "number",
|
|
"exclusiveMinimum": 0
|
|
},
|
|
"requests": {
|
|
"$ref": "#/definitions/resourceList"
|
|
},
|
|
"limits": {
|
|
"$ref": "#/definitions/resourceList"
|
|
}
|
|
}
|
|
},
|
|
"resourceList": {
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"cpu": {
|
|
"type": ["string", "number", "null"]
|
|
},
|
|
"memory": {
|
|
"type": ["string", "number", "null"]
|
|
},
|
|
"ephemeral-storage": {
|
|
"type": ["string", "number", "null"]
|
|
},
|
|
"ephemeralStorage": {
|
|
"type": ["string", "number", "null"]
|
|
}
|
|
}
|
|
},
|
|
"env": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["name"],
|
|
"properties": {
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"value": {
|
|
"type": "string"
|
|
},
|
|
"valueFrom": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"secretKeyRef": {
|
|
"$ref": "#/definitions/keyRef"
|
|
},
|
|
"configMapKeyRef": {
|
|
"$ref": "#/definitions/keyRef"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"keyRef": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["name", "key"],
|
|
"properties": {
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"key": {
|
|
"type": "string"
|
|
}
|
|
}
|
|
},
|
|
"securityContext": {
|
|
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99) instead of rendering an unhardened workload. For the server the pod context is consumed twice - as the pod's securityContext and by the /run-ownership init container, which chowns /run to runAsUser:runAsGroup because s6-overlay requires /run to be owned by the runtime uid.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"pod": {
|
|
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
|
|
"type": ["object", "null"]
|
|
},
|
|
"container": {
|
|
"description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.",
|
|
"type": ["object", "null"]
|
|
}
|
|
}
|
|
},
|
|
"probe": {
|
|
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing.",
|
|
"type": ["object", "null"]
|
|
}
|
|
}
|
|
}
|