From 8074ed14a5724bb56eef64f176e44449f1271a35 Mon Sep 17 00:00:00 2001 From: Justin Klein Date: Tue, 22 Sep 2026 19:59:37 +0000 Subject: [PATCH] Guard the four unguarded OnePasswordItem references with required() calibre-web-automated 7.0.0, minecraft-server 6.0.0 and urlaubsverwaltung 10.0.0 each majors: an installation that renders today with an empty itemPath stops rendering. searxng 1.0.6 is documentation only - its valkey reference is correctly optional and stays gated by {{- if }}. Every message names the values path. values.yaml now says per key why it is required, reusing the wording the charts' NOTES.txt already carry. The hand-rolled "cwaSecretRef IS UNSET" block in calibre-web-automated's NOTES.txt is gone: after the guard the render aborts first and that block is unreachable. minecraft-server's note stays true and now points at the guard. No "required" array in the schemas, and README guideline 12 now says why. The plan called for one; measuring it showed it could never fire: the key ships as null, so it is present at validation time, and JSON Schema's required tests presence, not null-ness. Making it fire means either dropping the null default or narrowing the type to "string", and both turn helm lint --strict red on the chart's own defaults. The schema therefore describes the key as required and required() enforces it. The README bullet on how null reaches the schema was also wrong in the case it was being applied to - a null chart default is not coalesced away - and now distinguishes the three cases. searxng: the claim "charts in this repo have no dependencies (README, guideline 13)" in the valkey template, in values.yaml and twice in _helpers.tpl has been false since #214 and cited the wrong guideline; dependencies are admissible and are guideline 14. The same sentence survives in other charts' helpers and needs its own round. --- README.md | 8 +++++++- calibre-web-automated/Chart.yaml | 2 +- calibre-web-automated/templates/NOTES.txt | 11 ++++------- .../templates/calibre-web-automated.secret.yaml | 2 +- calibre-web-automated/values.schema.json | 1 + calibre-web-automated/values.yaml | 9 +++++++++ minecraft-server/Chart.yaml | 2 +- minecraft-server/templates/NOTES.txt | 6 ++++-- .../templates/minecraft-server.secret.yaml | 2 +- minecraft-server/values.schema.json | 1 + minecraft-server/values.yaml | 12 +++++++++--- searxng/Chart.yaml | 2 +- searxng/templates/_helpers.tpl | 13 +++++++------ searxng/templates/searxng-valkey.secret.yaml | 5 ++++- searxng/values.yaml | 8 ++++---- urlaubsverwaltung/Chart.yaml | 2 +- .../templates/urlaubsverwaltung-db.secret.yaml | 2 +- .../templates/urlaubsverwaltung-oidc.secret.yaml | 2 +- urlaubsverwaltung/values.schema.json | 2 ++ urlaubsverwaltung/values.yaml | 14 ++++++++++++++ 20 files changed, 74 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 589ed9b..86bd087 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,13 @@ Reference implementation: `template-chart/values.schema.json` and `template-char **Two things about the mechanics that are easy to lose:** -- **`null` reaches the schema in one case and not the other.** For a key **with** a chart default, Helm's coalescing treats the `null` as a deletion and removes it *before* schema validation — the schema never sees it, and only the helper can catch it. For a key **without** a default (e.g. `resources.limits`), there is nothing to delete, the `null` survives into validation, and a strict `"type": "object"` aborts the render. That is why the helper **and** the widened `["object", "null"]` types are both needed: each covers a case the other cannot. Everything else stays strictly typed. +- **`null` reaches the schema in two cases out of three, and which one you are in depends on the chart's default, not on what the user wrote.** Measured on Helm 3.22.0 with a probe chart, because the wording here was wrong before and was applied to exactly the case it got wrong: + - a user `null` over a **non-null** chart default (`--set x=null`, or `x: null` in a values file) is treated as a deletion and the key is removed *before* schema validation — the schema never sees it, and only the helper can catch it; + - a **`null` chart default** survives into validation with or without a user `null` on top of it, because there is nothing to delete it against, and the schema sees the key present with value `null`; + - a key **without any default** (e.g. `resources.limits`) likewise survives, and a strict `"type": "object"` aborts the render. + + That is why the helper **and** the widened `["object", "null"]` / `["string", "null"]` types are both needed: each covers a case the other cannot. Everything else stays strictly typed. +- **A value that has no valid default cannot be enforced by `values.schema.json` at all, and `required()` is the whole enforcement.** The chain is short and it has no exit (#18): the key ships as `null`, so it is *present* at validation time, and JSON Schema's `required` tests presence and not null-ness — the array never fires, under any input. The two ways to make it fire both break the rule that `helm lint --strict` passes on chart defaults, since lint validates those defaults against the schema: dropping the `null` default gives `missing property ''`, and narrowing `["string", "null"]` to `"string"` gives `got null, want string` — each a red `validate-charts` run on a chart nobody touched. So such a key is guarded with `required()` in the template and **described** as required in the schema; a `required` array next to it would be a guard that cannot fire, which is the same defect as a value that does nothing, dressed as a safeguard. This is why no chart in this repo carries one. - **Validate every schema against the real values, not only against chart defaults.** Defaults are valid by construction, so they prove nothing. Render each chart against its bundle in `cluster-config/fleet-cd/` **and** against `helm get values` — the two differ more often than expected (four of ten bundles in one wave, always with the repo file being the worse one). This step caught a schema of mine that would have broken three live paperless releases, because all three write `tikaGotenberg.enabled` as the string `"true"` and the first draft demanded a boolean. Result per chart is a **list of rejected keys**, which is the cluster side's work order before the version is pulled up. ### 13. Everything else diff --git a/calibre-web-automated/Chart.yaml b/calibre-web-automated/Chart.yaml index 836f7bf..6834c16 100644 --- a/calibre-web-automated/Chart.yaml +++ b/calibre-web-automated/Chart.yaml @@ -5,7 +5,7 @@ name: calibre-web-automated description: A Helm chart to deploy calibere-web-automated icon: https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/main/README_images/cwa-logo-round-dark.png -version: 6.1.0+upv4.0.6 +version: 7.0.0+upv4.0.6 appVersion: v4.0.6 maintainers: diff --git a/calibre-web-automated/templates/NOTES.txt b/calibre-web-automated/templates/NOTES.txt index 3543977..f33c130 100644 --- a/calibre-web-automated/templates/NOTES.txt +++ b/calibre-web-automated/templates/NOTES.txt @@ -61,7 +61,7 @@ THE FIRST START BUILDS A LIBRARY, AND THE STARTUP PROBE IS TIMING IT kubectl -n {{ .Release.Namespace }} logs {{ $pod }} -f SECRETS — THE FIELDS THE 1PASSWORD ITEM MUST CARRY - {{ if .Values.secrets.cwaSecretRef }}{{ .Values.secrets.cwaSecretRef }}{{ else }}(secrets.cwaSecretRef is unset — see below){{ end }} + {{ .Values.secrets.cwaSecretRef }} → Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret hardcover-api-token becomes the HARDCOVER_TOKEN environment variable calibre-plugins mounted as /config/.config/calibre/customize.py.json, @@ -74,13 +74,10 @@ SECRETS — THE FIELDS THE 1PASSWORD ITEM MUST CARRY hardcover-api-token parks the pod in CreateContainerConfigError; a missing calibre-plugins fails the mount instead ("couldn't find key calibre-plugins"), and the container is never created at all. -{{- if not .Values.secrets.cwaSecretRef }} - secrets.cwaSecretRef IS UNSET. The OnePasswordItem has been created with an - empty itemPath, the operator has nothing to resolve, and the Secret above will - never appear — the pod cannot start. Set it to the item's op:// path and - upgrade. -{{- end }} + secrets.cwaSecretRef itself is required: the template guards it with + required(), so a release without it does not render at all rather than + creating a OnePasswordItem with an empty itemPath. CHANGING THE ITEM'S CONTENT RESTARTS NOTHING Helm never sees what is inside a OnePasswordItem, so it cannot hash it into the diff --git a/calibre-web-automated/templates/calibre-web-automated.secret.yaml b/calibre-web-automated/templates/calibre-web-automated.secret.yaml index 07d933c..4a1d3f6 100644 --- a/calibre-web-automated/templates/calibre-web-automated.secret.yaml +++ b/calibre-web-automated/templates/calibre-web-automated.secret.yaml @@ -3,4 +3,4 @@ kind: OnePasswordItem metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-secret spec: - itemPath: {{ .Values.secrets.cwaSecretRef }} + itemPath: {{ required "A 1Password item reference must be provided (secrets.cwaSecretRef)" .Values.secrets.cwaSecretRef }} diff --git a/calibre-web-automated/values.schema.json b/calibre-web-automated/values.schema.json index 95db901..0540605 100644 --- a/calibre-web-automated/values.schema.json +++ b/calibre-web-automated/values.schema.json @@ -17,6 +17,7 @@ "additionalProperties": false, "properties": { "cwaSecretRef": { + "description": "op:// item path carrying \"hardcover-api-token\" and \"calibre-plugins\". Required - the OnePasswordItem, the HARDCOVER_TOKEN env reference and the plugin volume are all rendered unconditionally. Enforced by required() in the template, not here: the null default keeps this type widened, and a schema \"required\" entry could never fire against a key that is present with value null.", "type": ["string", "null"] } } diff --git a/calibre-web-automated/values.yaml b/calibre-web-automated/values.yaml index ad59fba..27a1605 100644 --- a/calibre-web-automated/values.yaml +++ b/calibre-web-automated/values.yaml @@ -5,6 +5,15 @@ scaleDown: false secrets: + # REQUIRED - op:// item path for the 1Password operator, carrying the fields + # "hardcover-api-token" and "calibre-plugins". Both are needed even if you + # never use Hardcover: the OnePasswordItem is created unconditionally, and so + # are the environment reference and the volume. Without this value the render + # aborts - required() in calibre-web-automated.secret.yaml is the whole + # enforcement, and values.schema.json cannot help here (README, guideline 12). + # Before the guard existed it produced an OnePasswordItem with an empty + # itemPath, the operator had nothing to resolve, the Secret never appeared and + # the pod could not start. cwaSecretRef: null cwa: diff --git a/minecraft-server/Chart.yaml b/minecraft-server/Chart.yaml index 85dc145..66aca94 100644 --- a/minecraft-server/Chart.yaml +++ b/minecraft-server/Chart.yaml @@ -23,7 +23,7 @@ type: application # continuously, so a pod restart could pull a different image than the last one # without anything being recorded anywhere. The dated tags are immutable, so the # image is now pinned by the chart version like every other chart in this repo. -version: 5.0.0+up2026.8.2.java25 +version: 6.0.0+up2026.8.2.java25 appVersion: "2026.8.2-java25" maintainers: diff --git a/minecraft-server/templates/NOTES.txt b/minecraft-server/templates/NOTES.txt index 5d6bfd8..3d80928 100644 --- a/minecraft-server/templates/NOTES.txt +++ b/minecraft-server/templates/NOTES.txt @@ -204,9 +204,11 @@ OPERATORS ARE SYNCHRONIZED FROM VALUES, NOT FROM THE GAME SECRETS — WHICH FIELDS THE 1PASSWORD ITEM MUST CARRY The OnePasswordItem CR {{ $secret }} is rendered unconditionally, so secrets.minecraftServerRef has to be set even when nothing consumes a key from - it; it has no default and would otherwise render an itemPath of null. + it; it has no default and would otherwise render an itemPath of null. The + template guards it with required(), so a release without it does not render at + all — this note tells you why the guard is there, not that you got past it. - secrets.minecraftServerRef → {{ .Values.secrets.minecraftServerRef | default "NOT SET" }} + secrets.minecraftServerRef → {{ .Values.secrets.minecraftServerRef }} The operator copies the item's FIELDS into the Kubernetes Secret, one key per field name. This release consumes: diff --git a/minecraft-server/templates/minecraft-server.secret.yaml b/minecraft-server/templates/minecraft-server.secret.yaml index 6f899ca..1b110ce 100644 --- a/minecraft-server/templates/minecraft-server.secret.yaml +++ b/minecraft-server/templates/minecraft-server.secret.yaml @@ -3,4 +3,4 @@ kind: OnePasswordItem metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-secret spec: - itemPath: {{ .Values.secrets.minecraftServerRef }} + itemPath: {{ required "A 1Password item reference must be provided (secrets.minecraftServerRef) - the OnePasswordItem is rendered unconditionally, so it is needed even with RCON and CurseForge both off" .Values.secrets.minecraftServerRef }} diff --git a/minecraft-server/values.schema.json b/minecraft-server/values.schema.json index 680c9fe..68ecdc5 100644 --- a/minecraft-server/values.schema.json +++ b/minecraft-server/values.schema.json @@ -28,6 +28,7 @@ "additionalProperties": false, "properties": { "minecraftServerRef": { + "description": "op:// item path for the 1Password operator. Required even when RCON and the CurseForge downloader are both off: the OnePasswordItem is rendered unconditionally. The item's CONTENT may be empty in that case, the reference may not. Enforced by required() in the template, not here: the null default keeps this type widened, and a schema \"required\" entry could never fire against a key that is present with value null.", "type": ["string", "null"] } } diff --git a/minecraft-server/values.yaml b/minecraft-server/values.yaml index dd5420a..cc9cb45 100644 --- a/minecraft-server/values.yaml +++ b/minecraft-server/values.yaml @@ -37,9 +37,15 @@ image: tagOverride: "" secrets: - # op:// item path for the 1Password operator. Only needs to carry a - # "curseforge-api-key" key when curseforge.enabled is true and an - # "rcon-password" key when server.rcon.enabled is true. + # REQUIRED - op:// item path for the 1Password operator. The OnePasswordItem + # CR is rendered unconditionally, so this reference has to be set even when + # nothing consumes a key from it; it has no default and would otherwise render + # an itemPath of null. The item's CONTENT may be empty in that case: it only + # needs to carry a "curseforge-api-key" key when curseforge.enabled is true + # and an "rcon-password" key when server.rcon.enabled is true. Without the + # value the render aborts - required() in minecraft-server.secret.yaml is the + # whole enforcement, and values.schema.json cannot help here (README, + # guideline 12). # # Documented limit (chart 3.0.0): changing this path rolls the pod (the # StatefulSet hashes the rendered OnePasswordItem into checksum/secret-ref), diff --git a/searxng/Chart.yaml b/searxng/Chart.yaml index 8423968..be19d51 100644 --- a/searxng/Chart.yaml +++ b/searxng/Chart.yaml @@ -9,7 +9,7 @@ type: application # (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the # exact tag, and the +up suffix normalizes the dash to a dot as the versioning # scheme requires - the same shape rss-bridge and minecraft-server use. -version: 1.0.5+up2026.9.13.3b8009065 +version: 1.0.6+up2026.9.13.3b8009065 appVersion: "2026.9.13-3b8009065" maintainers: diff --git a/searxng/templates/_helpers.tpl b/searxng/templates/_helpers.tpl index a648573..af145b5 100644 --- a/searxng/templates/_helpers.tpl +++ b/searxng/templates/_helpers.tpl @@ -19,9 +19,10 @@ Usage (the values path to the resources dict varies per chart): resources: {{- include "searxng.resources" .Values..resources | nindent 12 }} -NOTE: charts in this repo deliberately have no dependencies, so this helper is -duplicated into every chart's templates/_helpers.tpl with the chart's name as -define prefix. template-chart holds the reference implementation. +NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with +the chart's name as define prefix, because sharing it would mean a library +chart, and a dependency is admissible but expensive (README, guideline 14) - +this chart takes none. template-chart holds the reference implementation. */}} {{- define "searxng.resources" -}} {{- $resources := . | default dict -}} @@ -168,9 +169,9 @@ Rules: - keys only present in the override are passed through unchanged. NOTE: like the helpers above, this is duplicated into every chart's -templates/_helpers.tpl with the chart's name as define prefix, because charts -in this repo deliberately have no dependencies. template-chart holds the -reference implementation. +templates/_helpers.tpl with the chart's name as define prefix, because sharing +it would mean a library chart and this chart takes no dependency (README, +guideline 14). template-chart holds the reference implementation. */}} {{- define "searxng.defaultedDict" -}} {{- $defaults := .defaults | default dict -}} diff --git a/searxng/templates/searxng-valkey.secret.yaml b/searxng/templates/searxng-valkey.secret.yaml index 037e9db..f039ea8 100644 --- a/searxng/templates/searxng-valkey.secret.yaml +++ b/searxng/templates/searxng-valkey.secret.yaml @@ -6,6 +6,9 @@ metadata: spec: # Optional, and only meaningful with settings.limiter: true. The item must # carry a "valkey-url" field pointing at an EXTERNAL Valkey/Redis; the chart - # deploys none of its own, because charts in this repo have no dependencies. + # deploys none of its own. Chart dependencies are admissible since #214 + # (README, guideline 14), but this chart declares none: an external connection + # through a secret is the same shape outline and paperless-ngx use for their + # databases. itemPath: {{ .Values.secrets.valkeySecretRef }} {{- end }} diff --git a/searxng/values.yaml b/searxng/values.yaml index 6861601..e984a5a 100644 --- a/searxng/values.yaml +++ b/searxng/values.yaml @@ -32,10 +32,10 @@ secrets: # unless the bot detection below is switched on. # # Setting it renders a second OnePasswordItem and passes the URL to the - # container as SEARXNG_VALKEY_URL. The chart ships NO Valkey of its own - - # charts in this repo have no dependencies (README, guideline 13), so an - # external connection through a secret is the same shape outline and - # paperless-ngx use for their databases. + # container as SEARXNG_VALKEY_URL. The chart ships NO Valkey of its own. + # Chart dependencies are admissible since #214 (README, guideline 14), but + # this chart declares none, so an external connection through a secret is the + # same shape outline and paperless-ngx use for their databases. valkeySecretRef: null # # Documented limitation (README, guideline 9): the CONTENT of these items is diff --git a/urlaubsverwaltung/Chart.yaml b/urlaubsverwaltung/Chart.yaml index 99b6f41..31c0b01 100644 --- a/urlaubsverwaltung/Chart.yaml +++ b/urlaubsverwaltung/Chart.yaml @@ -4,5 +4,5 @@ description: A Helm chart to deploy urlaubsverwaltung on Kubernetes icon: https://raw.githubusercontent.com/urlaubsverwaltung/urlaubsverwaltung/main/src/main/resources/static/favicons/apple-touch-icon.png type: application -version: 9.0.5+up6.13.1 +version: 10.0.0+up6.13.1 appVersion: 6.13.1 diff --git a/urlaubsverwaltung/templates/urlaubsverwaltung-db.secret.yaml b/urlaubsverwaltung/templates/urlaubsverwaltung-db.secret.yaml index 811154b..f5630dc 100644 --- a/urlaubsverwaltung/templates/urlaubsverwaltung-db.secret.yaml +++ b/urlaubsverwaltung/templates/urlaubsverwaltung-db.secret.yaml @@ -3,4 +3,4 @@ kind: OnePasswordItem metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret spec: - itemPath: {{ .Values.secrets.databaseSecretRef }} + itemPath: {{ required "A database Secret Reference must be provided (secrets.databaseSecretRef)" .Values.secrets.databaseSecretRef }} diff --git a/urlaubsverwaltung/templates/urlaubsverwaltung-oidc.secret.yaml b/urlaubsverwaltung/templates/urlaubsverwaltung-oidc.secret.yaml index d19c0e2..c5ffbc1 100644 --- a/urlaubsverwaltung/templates/urlaubsverwaltung-oidc.secret.yaml +++ b/urlaubsverwaltung/templates/urlaubsverwaltung-oidc.secret.yaml @@ -3,4 +3,4 @@ kind: OnePasswordItem metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-oidc-secret spec: - itemPath: {{ .Values.secrets.oidcSecretRef }} + itemPath: {{ required "An OIDC Secret Reference must be provided (secrets.oidcSecretRef)" .Values.secrets.oidcSecretRef }} diff --git a/urlaubsverwaltung/values.schema.json b/urlaubsverwaltung/values.schema.json index 5529d32..8b327c9 100644 --- a/urlaubsverwaltung/values.schema.json +++ b/urlaubsverwaltung/values.schema.json @@ -17,9 +17,11 @@ "additionalProperties": false, "properties": { "databaseSecretRef": { + "description": "op:// item path carrying \"jdbc-url\", \"username\" and \"password\". Required - the chart deploys no database and all state lives in the external one. Enforced by required() in the template, not here: the null default keeps this type widened, and a schema \"required\" entry could never fire against a key that is present with value null.", "type": ["string", "null"] }, "oidcSecretRef": { + "description": "op:// item path carrying \"client-id\", \"client-secret\", \"issuer-name\", \"issuer-uri\", \"jwt-issuer-uri\" and \"redirect-uri\". Required - the chart offers no non-OIDC login and the app resolves the issuer at boot. Enforced by required() in the template, not here: the null default keeps this type widened, and a schema \"required\" entry could never fire against a key that is present with value null.", "type": ["string", "null"] } } diff --git a/urlaubsverwaltung/values.yaml b/urlaubsverwaltung/values.yaml index 0d22d95..45745b1 100644 --- a/urlaubsverwaltung/values.yaml +++ b/urlaubsverwaltung/values.yaml @@ -5,7 +5,21 @@ scaleDown: false secrets: + # REQUIRED - op:// item path carrying "jdbc-url", "username" and "password". + # This chart deploys NO database; all state lives in the external one, and + # three datasource environment variables read these keys unconditionally. + # Without the value the render aborts - required() in + # urlaubsverwaltung-db.secret.yaml is the whole enforcement, and + # values.schema.json cannot help here (README, guideline 12). databaseSecretRef: null + # REQUIRED - op:// item path carrying "client-id", "client-secret", + # "issuer-name", "issuer-uri", "jwt-issuer-uri" and "redirect-uri". This chart + # offers no login path besides OIDC, and the identity provider is required at + # boot, not at login: the application resolves /.well-known/ + # openid-configuration while building its security beans, before the HTTP port + # opens, and exits when it cannot. Without the value the render aborts - + # required() in urlaubsverwaltung-oidc.secret.yaml is the whole enforcement, + # and values.schema.json cannot help here (README, guideline 12). oidcSecretRef: null ingress: -- 2.54.0