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: