OnePasswordItem references: required() for the four that are not optional #38

Merged
jklein merged 1 commits from fix/onepassworditem-required-guards into main 2026-09-26 23:34:46 +02:00
20 changed files with 74 additions and 32 deletions

No files matched your search

+7 -1
View File
@@ -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 '<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.5+up6.13.1
version: 10.0.0+up6.13.1
appVersion: 6.13.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: