required() for the four mandatory OnePasswordItem references (#18)
Chart pages: calibre-web-automated 7.0.0, minecraft-server 6.0.0 and urlaubsverwaltung 10.0.0 now guard their secret references, so the values tables no longer say "not guarded by a required in the template". searxng 1.0.6 is a version snapshot only - its valkey reference was already correctly optional and stays that way. Style guide 12: the bullet on how null reaches the schema was wrong in the case it was being applied to. A null chart default is NOT coalesced away; only a user null over a non-null default is. Added the rule that follows from it - a value with no valid default cannot be enforced by values-schema at all while helm lint --strict has to pass on the defaults, so required() is the whole enforcement and a required array would be a guard that cannot fire. Lessons learned: both halves of that, since a safeguard that looks like one and never fires is the same failure shape as the missing guard it replaced.
6 files changed
+31
-16
No files matched your search
+7
-1
@@ -172,7 +172,13 @@ Two mechanics come with it: Go's `not` treats **every non-empty string as true**
|
||||
|
||||
### Two 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* 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. 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 being 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* 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: 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. Everything else stays strictly typed.
|
||||
- **A value that has no valid default cannot be enforced by `values.schema.json` at all — `required()` is the whole enforcement.** 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. Both ways of making it fire break the rule that `helm lint --strict` passes on chart defaults, because lint validates those defaults — dropping the `null` default gives `missing property '<key>'`, narrowing `["string", "null"]` to `"string"` gives `got null, want string`, each a red pipeline on a chart nobody touched. So such a key is guarded with `required()` in the template and **described** as required in the schema, and carries no `required` array. See [Lessons learned](Lessons-Learned#a-guard-that-never-fires-still-looks-like-a-guard).
|
||||
- **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 cluster bundle **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 caught a schema that would have broken three live paperless releases, because all three write `tikaGotenberg.enabled` as the string `"true"` while the first draft demanded a boolean. The 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
|
||||
|
||||
@@ -130,10 +130,19 @@ Kubernetes cannot order two regular containers, and an init container waiting fo
|
||||
|
||||
**Now:** a start gate in front of the image entrypoint polls gluetun's health server and **exits** when the timeout passes instead of starting anyway — a crash-looping container is visible, a silently empty indexer is not.
|
||||
|
||||
## A guard that never fires still looks like a guard
|
||||
|
||||
Five `OnePasswordItem` templates rendered `itemPath: {{ .Values.secrets.<x>Ref }}` without a `required()`. Unset, the CR is created with an **empty** `itemPath`: `helm install` succeeds, the object exists, and nothing reports a problem. The operator has nothing to resolve, the Kubernetes Secret never appears, and the failure arrives minutes later in the pod — `CreateContainerConfigError`, or a mount that fails with `couldn't find key ...` so the container is never created at all. Four of the five were not optional; the fifth, searxng's valkey reference, was correctly optional and gated with `{{- if }}`, which is the other half of the rule.
|
||||
|
||||
The second finding came out of fixing the first, and is the more general one. The plan was to back each guard with a `required` array in `values.schema.json`. Measured on Helm 3.22.0 with a probe chart, that array **could never fire**: these keys ship with `null` as their default, so the key is *present* at validation time, and JSON Schema's `required` tests presence, not null-ness. Only a user `null` written over a **non-null** default is coalesced away — a `null` default is not. And both ways of making the array fire (dropping the default, or narrowing `["string", "null"]` to `"string"`) turn `helm lint --strict` red on the chart's own defaults, which the pipeline runs.
|
||||
|
||||
**Now:** a value with no valid default is enforced by `required()` in the template, with a message naming the values path, and **described** as required in the schema — never by a `required` array, which would be a safeguard that cannot fire, dressed as one that can. An optional reference says so in `values.yaml` *and* in the schema description, and its template renders nothing when the value is empty. See [§12](Chart-Style-Guide#12-values-that-must-not-lie).
|
||||
|
||||
## The recurring shape
|
||||
|
||||
- **Green is a claim about the assertions that ran, not about the system.**
|
||||
- **A failure that looks like success is more expensive than a loud one** — which is why several of these fixes deliberately turn a silent state into a crash.
|
||||
- **The environment can hide a bug for years.** Portability work is also archaeology.
|
||||
- **When a rename and a silent behaviour change are both available, take the rename.** Schema validation failing loudly beats a value that quietly means the opposite.
|
||||
- **A safeguard is only a safeguard if it can fail.** Both the missing `required()` and the `required` array that replaced it read as protection; neither could ever have fired.
|
||||
- **Write down what was measured, and say when something was not.** Half of this page exists because someone recorded a number instead of an impression.
|
||||
+4
-4
@@ -4,7 +4,7 @@ Deploys [Calibre-Web Automated](https://github.com/crocodilestick/Calibre-Web-Au
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Chart version | `6.1.0+upv4.0.6` |
|
||||
| Chart version | `7.0.0+upv4.0.6` |
|
||||
| App version | `v4.0.6` (image `ghcr.io/crocodilestick/calibre-web-automated`) |
|
||||
| Workloads | StatefulSet (1 replica) |
|
||||
| Storage | one RWO PVC (`cwa.storage`, mounted three times by `subPath`) + one **RWX** consume PVC fixed at `1Gi` |
|
||||
@@ -60,7 +60,7 @@ The chart renders one `OnePasswordItem`; the operator turns it into a Kubernetes
|
||||
|---|---|---|
|
||||
| `secrets.cwaSecretRef` | `<release>-calibre-web-automated-secret` | `hardcover-api-token` → the `HARDCOVER_TOKEN` environment variable; `calibre-plugins` → mounted as `/config/.config/calibre/customize.py.json`, Calibre's plugin registry (`{}` is a valid empty value) |
|
||||
|
||||
**Both fields are required even if Hardcover is never used**: the CR, the environment reference and the volume are created unconditionally. The two missing-field failures differ — a missing `hardcover-api-token` parks the pod in `CreateContainerConfigError`, a missing `calibre-plugins` fails the *mount* (`couldn't find key calibre-plugins`) and the container is never created at all. With `secrets.cwaSecretRef` unset the CR is created with an empty `itemPath`, the operator has nothing to resolve, the Secret never appears and the pod cannot start.
|
||||
**Both fields are required even if Hardcover is never used**: the CR, the environment reference and the volume are created unconditionally. The two missing-field failures differ — a missing `hardcover-api-token` parks the pod in `CreateContainerConfigError`, a missing `calibre-plugins` fails the *mount* (`couldn't find key calibre-plugins`) and the container is never created at all. `secrets.cwaSecretRef` itself is guarded with `required()` since chart 7.0.0: left unset, the render aborts. Until then it produced a CR with an empty `itemPath` — the operator had nothing to resolve, the Secret never appeared and the pod could not start, with nothing in the install output saying so.
|
||||
|
||||
## Configurable values
|
||||
|
||||
@@ -75,7 +75,7 @@ The chart renders one `OnePasswordItem`; the operator turns it into a Kubernetes
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `secrets.cwaSecretRef` | string | `null` | **Required.** `op://` path of the item carrying `hardcover-api-token` and `calibre-plugins`. Not guarded by a `required` in the template — an unset value renders and installs, and the pod then never starts. |
|
||||
| `secrets.cwaSecretRef` | string | `null` | **Required**, guarded by `required()` since 7.0.0 — unset, the render aborts with `A 1Password item reference must be provided (secrets.cwaSecretRef)`. `op://` path of the item carrying `hardcover-api-token` and `calibre-plugins`. |
|
||||
|
||||
### `cwa`
|
||||
|
||||
@@ -185,6 +185,6 @@ kubectl -n <namespace> logs <release>-calibre-web-automated-sfs-0 -f
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — the rules this chart implements, and §12.4, whose worked example it is
|
||||
- [Lessons learned](Lessons-Learned) — the uid-911 finding in general form
|
||||
- [Versioning](Versioning) — what a bump of `6.1.0+upv4.0.6` means
|
||||
- [Versioning](Versioning) — what a bump of `7.0.0+upv4.0.6` means
|
||||
- [Renovate](Renovate) — how `appVersion` gets updated
|
||||
- [Chart index](Charts)
|
||||
+5
-5
@@ -4,7 +4,7 @@ Deploys a Minecraft server ([itzg/minecraft-server](https://github.com/itzg/dock
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Chart version | `5.0.0+up2026.8.2.java25` |
|
||||
| Chart version | `6.0.0+up2026.8.2.java25` |
|
||||
| App version | `2026.8.2-java25` (image `itzg/minecraft-server`) — the tag fixes the JVM |
|
||||
| Workloads | StatefulSet (1 replica), plus an init container with `curseforgeServerpack` |
|
||||
| Storage | one RWO PVC, `<release>-mc-volume-claim`, two subPaths (`mc-data` → `/data`, `mc-modpacks` → `/modpacks`) |
|
||||
@@ -56,7 +56,7 @@ The chart renders a `OnePasswordItem` CR; the operator turns it into a Kubernete
|
||||
|---|---|---|
|
||||
| `secrets.minecraftServerRef` | `<release>-minecraft-server-secret` | `rcon-password` → `RCON_PASSWORD`, **only when `server.rcon.enabled` is true**; `curseforge-api-key` → `CF_API_KEY`, **only when `curseforge.enabled` is true** |
|
||||
|
||||
With both features off the item may be empty — but the values key is still required, or the CR renders an `itemPath` of `null`. What the operator actually delivered:
|
||||
With both features off the item may be empty — but the values key is still required. Since chart 6.0.0 `required()` enforces that: unset, the render aborts instead of producing a CR with an `itemPath` of `null`. What the operator actually delivered:
|
||||
|
||||
```bash
|
||||
kubectl -n <namespace> get secret <release>-minecraft-server-secret -o jsonpath='{.data}'
|
||||
@@ -83,7 +83,7 @@ The whole `image` block may be `null`; the tag then falls back to `appVersion`,
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `secrets.minecraftServerRef` | string | `null` | **Required, always** — the CR is rendered unconditionally. `op://` item path; the item needs fields only for the features actually switched on. |
|
||||
| `secrets.minecraftServerRef` | string | `null` | **Required, always** — the CR is rendered unconditionally, and `required()` enforces the value since 6.0.0 (the render aborts, naming the key). `op://` item path; the item needs fields only for the features actually switched on. |
|
||||
|
||||
### `server`
|
||||
|
||||
@@ -209,7 +209,7 @@ minecraft:
|
||||
version: "1.21.11"
|
||||
```
|
||||
|
||||
It is called `tagOverride`, not `tag`, because it **decouples the running image from the chart version**: `helm list` then reports `5.0.0+up2026.8.2.java25` while the pod runs something else, and `NOTES.txt` says so. Prefer moving to the current line; use this when a modpack pins you to an older Minecraft version.
|
||||
It is called `tagOverride`, not `tag`, because it **decouples the running image from the chart version**: `helm list` then reports `6.0.0+up2026.8.2.java25` while the pod runs something else, and `NOTES.txt` says so. Prefer moving to the current line; use this when a modpack pins you to an older Minecraft version.
|
||||
|
||||
It is also **deliberately outside Renovate's reach** — `renovate.json5` tracks `appVersion` in `Chart.yaml` and nothing else. A tag set here is never bumped by anything; it is yours to review.
|
||||
|
||||
@@ -326,7 +326,7 @@ The install-test runs on every PR that touches the chart — see [CI/CD](CI-CD).
|
||||
## See also
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — the rules this chart implements; it is the pilot for the strict `values.schema.json`
|
||||
- [Versioning](Versioning) — what a bump of `5.0.0+up2026.8.2.java25` means, and why the `+up` suffix normalizes the dash
|
||||
- [Versioning](Versioning) — what a bump of `6.0.0+up2026.8.2.java25` means, and why the `+up` suffix normalizes the dash
|
||||
- [Renovate](Renovate) — the Java-line compatibility group
|
||||
- [Lessons learned](Lessons-Learned)
|
||||
- [Chart index](Charts)
|
||||
+2
-2
@@ -4,7 +4,7 @@ Deploys [SearXNG](https://docs.searxng.org/), a metasearch engine, as a stateles
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Chart version | `1.0.5+up2026.9.13.3b8009065` |
|
||||
| Chart version | `1.0.6+up2026.9.13.3b8009065` |
|
||||
| App version | `2026.9.13-3b8009065` (image `docker.io/searxng/searxng`) |
|
||||
| Workloads | Deployment (1 replica) |
|
||||
| Storage | none — two emptyDirs (`/tmp`, `/var/cache/searxng`) |
|
||||
@@ -221,7 +221,7 @@ The install-test runs on every PR that touches the chart — see [CI/CD](CI-CD).
|
||||
## See also
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — the rules this chart implements
|
||||
- [Versioning](Versioning) — what a bump of `1.0.5+up2026.9.13.3b8009065` means
|
||||
- [Versioning](Versioning) — what a bump of `1.0.6+up2026.9.13.3b8009065` means
|
||||
- [Renovate](Renovate) — why this chart carries a `versioning` override
|
||||
- [Lessons learned](Lessons-Learned)
|
||||
- [Chart index](Charts)
|
||||
+4
-4
@@ -4,7 +4,7 @@ Deploys [Urlaubsverwaltung](https://urlaubsverwaltung.cloud/), a leave and absen
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Chart version | `9.0.2+up6.12.0` |
|
||||
| Chart version | `10.0.0+up6.12.1` |
|
||||
| App version | `6.12.0` (image `docker.io/urlaubsverwaltung/urlaubsverwaltung`) |
|
||||
| Workloads | Deployment (1 replica) |
|
||||
| Storage | none — `/tmp` is an emptyDir, all state is in the external database |
|
||||
@@ -76,8 +76,8 @@ kubectl -n <namespace> get secret <release>-urlaubsverwaltung-db-secret -o jsonp
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `secrets.databaseSecretRef` | string | `null` | **Required.** `op://` path of the database item. Not guarded by a `required` in the template: left unset the CR is created with an empty `itemPath`, the render and the install succeed, and the pod never starts. |
|
||||
| `secrets.oidcSecretRef` | string | `null` | **Required.** `op://` path of the OIDC item. Same failure mode. |
|
||||
| `secrets.databaseSecretRef` | string | `null` | **Required**, guarded by `required()` since 10.0.0 — unset, the render aborts with `A database Secret Reference must be provided (secrets.databaseSecretRef)`. `op://` path of the database item. Before 10.0.0 an unset value rendered a CR with an empty `itemPath`, installed cleanly, and the pod never started. |
|
||||
| `secrets.oidcSecretRef` | string | `null` | **Required**, same guard since 10.0.0. `op://` path of the OIDC item. There is no non-OIDC login path in this chart, and the provider is resolved at boot rather than at login — see [The identity provider is required at boot](#the-identity-provider-is-required-at-boot-not-at-login). |
|
||||
|
||||
### `urlaubsverwaltung`
|
||||
|
||||
@@ -215,6 +215,6 @@ kubectl -n <namespace> rollout restart deployment/<release>-urlaubsverwaltung-de
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — the rules this chart implements, §10 and §12.3 of which it measured
|
||||
- [Lessons learned](Lessons-Learned) — the readiness-group and additive-env findings in general form
|
||||
- [Versioning](Versioning) — what a bump of `9.0.2+up6.12.0` means
|
||||
- [Versioning](Versioning) — what a bump of `10.0.0+up6.12.1` means
|
||||
- [Renovate](Renovate) — how `appVersion` gets updated
|
||||
- [Chart index](Charts)
|
||||
Reference in new issue
Block a user