Author SHA1 Message Date
jklein 6371f60eeb Guard the four unguarded OnePasswordItem references with required()
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 7s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 20s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Successful in 2m52s
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.
2026-09-22 20:03:27 +00:00
20 changed files with 74 additions and 32 deletions

No files matched your search

+7 -1
View File
@@ -356,7 +356,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 '<key>'`, 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
+1 -1
View File
@@ -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:
+4 -7
View File
@@ -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
@@ -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 }}
+1
View File
@@ -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"]
}
}
+9
View File
@@ -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:
+1 -1
View File
@@ -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:
+4 -2
View File
@@ -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:
@@ -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 }}
+1
View File
@@ -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"]
}
}
+9 -3
View File
@@ -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),
+1 -1
View File
@@ -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:
+7 -6
View File
@@ -19,9 +19,10 @@ Usage (the values path to the resources dict varies per chart):
resources:
{{- include "searxng.resources" .Values.<app>.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 -}}
+4 -1
View File
@@ -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 }}
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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.3+up6.12.1
version: 10.0.0+up6.12.1
appVersion: 6.12.1
@@ -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 }}
@@ -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 }}
+2
View File
@@ -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"]
}
}
+14
View File
@@ -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 <issuer-uri>/.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: