Commit Graph
100 Commits
Author SHA1 Message Date
jklein e666cfed6c rss-bridge: render byte counts as plain decimals (1.0.2+up2025.08.05)
Teil von #157.

The null-safe fallback helpers from #99 pass values through fromYaml, which
goes over JSON and therefore parses every number as float64. Go's "%v"
switches float64 to exponential notation from about 1e6 upwards, so a bare
byte count like 1048576 turned into "1.048576e+06" - not a valid Kubernetes
quantity. The suffix check in multiplyQuantity then aborted the render with
  unsupported resource quantity suffix "e+06" in "1.048576e+06"

The second "%v" - the one printing a non-integer product - never reaches that
check and failed silently instead: with a fractional limitFactor it rendered
"1.4999985e+06" into the manifest, which only the API server rejects on apply.

Both call sites now go through a new rss-bridge.formatNumber helper that
forces decimal notation via printf "%.10f" and trims the padding zeros again;
the leading dot in the regex protects "100.0000000000" from collapsing to
"1". Strings such as "512Mi" pass through untouched.

This is the canonical fix from #157, applied verbatim with this chart's
prefix; the round-trip itself is not touched.

Note on this chart specifically: rss-bridge sets explicit memory and
ephemeral-storage limits as chart defaults, and an explicit limit always
wins, so multiplyQuantity is not reachable through this chart's values at
all - the bug was reproduced and the fix verified on a throwaway copy with
those default limits removed. The rendered output for the chart defaults and
for ci/rss-bridge/test-values.yaml is byte-identical to main.

Patch bump: no behavior and no values-schema change.
2026-08-29 01:13:57 +02:00
jklein 11180bd503 bentopdf: render byte counts as plain decimals (1.0.1+up2.8.7)
multiplyQuantity turned its input into a string with printf "%v". The
null-safe fallback helpers from #99 pass values through fromYaml, which
parses every number as float64, and Go's "%v" switches float64 to
exponential notation from about 1e6 upwards. A bare byte count like
1048576 therefore arrived as "1.048576e+06", the suffix check rejected
"e+06" and the render aborted.

The same printf sat on the second output path, for a non-integer product
(a fractional limitFactor - typed "number", not "integer", in this
chart's values.schema.json, so ordinary values reach it). That one never
passes the suffix check, so it produced an invalid quantity silently and
only the API server would have rejected the manifest on apply.

Both are now routed through a formatNumber helper that forces decimal
notation and trims the padding zeros. Strings ("512Mi") pass through
untouched, so the default render is unchanged.

Part of #157. The helper is a verbatim copy of the fix already merged
for template-chart, zipline, voucher-vault and outline - multiplyQuantity
is the same duplicated code in every chart.
2026-08-29 01:04:40 +02:00
jklein c371179d76 cyberchef: format quantities without exponential notation (9.0.1+up11.4.0)
multiplyQuantity turned its input into a string with printf "%v". The null-safe fallback helpers from #99 route values through fromYaml, which parses every number as float64, and Go switches float64 to exponential notation from about 1e6 upwards. A bare byte count like 1048576 became "1.048576e+06" and the suffix check aborted the render.

The non-integer product used the same printf "%v" on output. That path never reaches the suffix check, so it rendered an invalid quantity silently, which only the API server rejects on apply. cyberchef types limitFactor as "number", so a fractional factor is allowed by the schema and that path is reachable with ordinary values.

Both call sites now go through a new formatNumber helper that forces decimal notation and trims the padding zeros; strings such as "512Mi" pass through untouched. Round-trip and resources helper are unchanged. The wording matches the canonical fix already merged for template-chart and the other charts, so the shared copy stays free of drift.
2026-08-29 01:03:19 +02:00
jklein 6ae380e8ec teamspeak-server: NOTES.txt and the exponential-quantity fix (5.1.0+up6.0.0.beta12.1)
Teil von #157 und Teil von #43.

#157: multiplyQuantity turned its input into a string with printf "%v". The
null-safe fallback helpers from #99 route values through fromYaml, which
parses every number as float64, and Go switches float64 to exponential
notation from about 1e6 upwards. A request of 1048576 therefore reached the
suffix check as "1.048576e+06" and aborted the render. The second printf
"%v", the one that prints a non-integer product, never reaches that check at
all: with a fractional limitFactor it wrote an invalid quantity into the
manifest without a word. Both sites now go through the canonical
teamspeak-server.formatNumber helper, identical to the other charts.

Note for this chart: values.schema.json types resources.limitFactor as
"number", not "integer", so the silent path is reachable with ordinary
values rather than only with --set-json.

#43: adds templates/NOTES.txt. teamspeak-server is a NodePort service with
no Ingress and no secrets at all, so the notes cover what is actually worth
looking up. Measured against the pinned image 6.0.0-beta12.1 rather than
taken from upstream docs: the first start prints a ServerAdmin privilege key
and ServerQuery credentials exactly once, and only the privilege key is
recoverable afterwards from the log files on the data volume; TSSERVER_
DEFAULT_PORT is read only when the virtual server is created, so changing
server.voicePort on an existing release moves the Service, the nodePort and
the containerPort while the server keeps answering on the old port, with all
three probes staying green because they only ever TCP-connect to the file
transfer port; TSSERVER_FILE_TRANSFER_PORT, by contrast, applies on every
start; the bundled license runs 2026-07-01 to 2026-10-01 with one virtual
server and 32 slots; and no query port listens at all, which leaves the
declared web-query container port 10080 bound by nothing.

Minor bump: additive, appVersion unchanged.
2026-08-29 01:00:15 +02:00
jklein cc1e7bb53e apache-tika: render byte counts as plain decimals (10.0.1+up3.3.1.0.full)
The null-safe fallback helpers from #99 pass values through fromYaml, which
goes over JSON and therefore parses every number as float64. Go's "%v"
switches float64 to exponential notation from about 1e6 upwards, so a bare
byte count like 1048576 reached multiplyQuantity as "1.048576e+06" and the
suffix check aborted the render:

  unsupported resource quantity suffix "e+06" in "1.048576e+06"

The second "%v" — the output of a non-integer product — never reaches that
check, so it produced an invalid quantity without any abort. In this chart
that value is caught one step later by NOTES.txt, which feeds the computed
memory limit back into multiplyQuantity: the Deployment still carried
memory: "1.4999985e+06" and the error pointed at NOTES.txt:54 instead of the
real cause.

Both places now go through a new formatNumber helper, which forces decimal
notation via "%.10f" and trims the padded zeros again; strings such as
"512Mi" pass through untouched. This is the canonical fix from #157, taken
over verbatim with this chart's prefix, because multiplyQuantity is the same
copy in every chart.

Chart defaults render unchanged. Patch bump: bug fix only, no behavior and no
values-schema change; appVersion stays.
2026-08-29 00:58:17 +02:00
jklein a915c5da5d satisfactory: exponential-notation fix and NOTES.txt (6.1.0+upv1.9.10)
Teil von #157 und Teil von #43.

#157: multiplyQuantity turned a bare byte count into exponential notation.
The null-safe fallback helpers pass values through fromYaml, which parses
every number as float64, and Go's "%v" switches float64 to exponential
notation from about 1e6 upwards. A request of 1048576 became
"1.048576e+06" and the render aborted on the suffix check.

Both printf "%v" sites are replaced by the canonical satisfactory.formatNumber
helper: the input of multiplyQuantity and the output of a non-integer product.
The second one never reached the suffix check, so it did not abort - it
rendered a silently invalid quantity that only the API server would reject.

#43: adds templates/NOTES.txt. The chart has no Ingress and no secrets, so
the notes cover what an operator would otherwise read out of the templates:
the NodePort a player actually dials and the pinned-port collision between
two releases, the 20-minute Steam download budget and that SKIPUPDATE is
false on every start, what a green pod does and does not prove (no probe
ever touches the UDP game port), that saves and game files share one PVC on
a hardcoded longhorn class which helm uninstall deletes, the emptyDirs
counting against the ephemeral-storage limit, the deliberate limitFactor 2
and fsGroupChangePolicy OnRootMismatch, and that game.seasonalEvents is
passed through to DISABLESEASONALEVENTS uninverted.

No rendered Kubernetes object changes, so the bump is minor; appVersion
stays v1.9.10.
2026-08-29 00:51:50 +02:00
jklein 021df7d024 urlaubsverwaltung: NOTES.txt and the exponential-quantity fix (8.1.0+up6.7.0)
Teil von #157 und Teil von #43.

#157: multiplyQuantity turned a bare byte count into exponential notation.
Values pass through fromYaml, which parses every number as float64, and Go's
"%v" switches float64 to exponential notation from about 1e6 upwards. Both
places that stringify a number now go through the new formatNumber helper -
the input of multiplyQuantity, which aborted the render with an unsupported
suffix "e+06", and the output of a non-integer product, which did not abort
at all but rendered an invalid quantity that only the API server rejects.

Measured on this chart against ci/urlaubsverwaltung/test-values.yaml:

  requests.memory=1048576              before: abort, suffix "e+06"
                                       after:  request 1048576, limit "3145728"
  requests.ephemeralStorage=2097152    before: abort, suffix "e+06"
                                       after:  limit "6291456"
  memory=999999, limitFactor=1.5       before: limit "1.4999985e+06", no abort
                                       after:  limit "1499998.5"
  requests.memory=65536 (regression)   196608 before and after

The correction is the canonical one, word for word as merged into
template-chart, voucher-vault and zipline; only the helper prefix differs.
urlaubsverwaltung has a single resources path and a single render site
(the Deployment), and both were verified.

#43: adds a NOTES.txt. Its content was verified against a running release in
a throwaway k3d cluster rather than taken from upstream documentation - the
identity provider being resolved at boot (a bad issuer-uri crashlooops the
pod instead of breaking the login), the buildpack sizing the JVM heap from
the computed memory limit, /actuator/health answering 503 for the lifetime of
the release because the chart points the mail sender at localhost:1025, the
probe groups not reacting to a database outage, and a name collision in
urlaubsverwaltung.env making the next helm upgrade fail on the patch order.

README.md is deliberately untouched: the line listing the charts that ship a
NOTES.txt collides with every parallel NOTES PR and is pulled up once at the
end of the programme.

Minor bump: a NOTES.txt renders no Kubernetes object and the quantity fix
changes no default. appVersion unchanged.
2026-08-29 00:50:17 +02:00
jklein 5c5160a920 minecraft-server: NOTES.txt and the exponential-quantity fix (4.1.0+up2026.8.2.java21)
Teil von #157 and Teil von #43, in one change because both are additive and
neither moves a rendered object.

#157 - multiplyQuantity turned large numbers into exponential notation. The
null-safe fallback helpers pass values through fromYaml, which parses every
number as float64, and Go's "%v" switches float64 to exponential notation from
about 1e6 upwards. A bare byte count like 1048576 became "1.048576e+06".

The chart has two affected positions, and they fail differently:

  - the entry of multiplyQuantity aborts the render loudly, because the
    exponent is caught by the suffix check
    (unsupported resource quantity suffix "e+06").
  - the output of a non-integer product never reaches that check and renders
    silently: a fractional limitFactor together with a bare number produced
    "1.4999985e+06" in the manifest, rejected only later by the API server.

Note that this chart's schema types limitFactor as "number" with
exclusiveMinimum 0, not as an integer, so the silent path is reachable here
through ordinary values.

Both positions now go through a new minecraft-server.formatNumber helper, the
canonical correction already applied to template-chart, voucher-vault, zipline,
outline and minecraft-router.

#43 - NOTES.txt for this chart. It prints what an operator would otherwise have
to read out of the templates, conditional on the values actually set:

  - access, which is not a URL: ClusterIP means the server is reachable only
    once its Service name is entered in the minecraft-router release's static
    mappings, since that router does no auto-discovery; NodePort prints the
    node port and warns when it falls outside 30000-32767.
  - the Java line, derived from appVersion rather than written into the file,
    so it holds for all three published lines, together with the fact that
    nothing checks minecraft.version against it.
  - what a green rollout does not prove. The probes do run a real server-list
    ping, so Ready means the server answers - but a ping is answered before any
    login, so every login-side refusal is invisible to all three probes. The
    whitelist is the practical case: enabled without players locks everyone out
    while the rollout stays green.
  - which fields the 1Password item must carry, listed only for the features
    actually enabled, plus the documented gap that a changed item content does
    not roll the pod.
  - the world PVC that helm uninstall deletes, the two memory numbers and how
    they decouple, RCON not being published by the Service, the declared but
    unserved metrics port, and the enabled modpack source.

No real domain anywhere; the router example uses mc.example.com.

Verified: helm lint --strict clean on all three Java lines; rendering the CI
values and the chart defaults is byte-identical to origin/main, so no
Kubernetes object changes and the minor bump is the right one.
2026-08-29 00:43:22 +02:00
jklein 0d2fa19cf1 samba: NOTES.txt and the exponential-quantity fix (7.1.0+up4.23.10)
Teil von #157 und Teil von #43.

#157: multiplyQuantity turned its input into a string with printf "%v".
The null-safe fallback helpers from #99 route values through fromYaml, which
parses every number as float64, and Go switches float64 to exponential
notation from about 1e6 upwards. A request of 1048576 therefore reached the
suffix check as "1.048576e+06" and aborted the render. The second printf
"%v", the one that prints a non-integer product, never reaches that check at
all: with a fractional limitFactor it wrote an invalid quantity into the
manifest without a word. Both sites now go through the canonical
samba.formatNumber helper, identical to the other charts.

#43: adds templates/NOTES.txt. samba is a NodePort service, not a web app,
so the notes cover the SMB access path per client, the SMB3/encryption
requirements the chart pins in smb.conf, the users.conf field the 1Password
item must carry, that a restart drops every open SMB session, and that the
share PVCs are deleted by helm uninstall and re-created empty on a rename.

Minor bump: additive, appVersion unchanged.
2026-08-29 00:41:13 +02:00
jklein c6583468c7 minecraft-router: NOTES.txt and exponential-notation fix (4.1.0+up1.46.4)
Teil von #157: multiplyQuantity turned every number from the null-safe
fallback helpers into a float64 via fromYaml, and Go's "%v" switches
float64 to exponential notation from about 1e6 upwards. A bare byte count
like 1048576 became "1.048576e+06": the integer path aborted the render
on the suffix check, while the non-integer path (a fractional limitFactor)
rendered the invalid quantity silently and only failed at the API server.
Both string conversions now go through the canonical formatNumber helper,
identical to the fix already merged for template-chart, voucher-vault and
zipline.

Teil von #43: adds a NOTES.txt. minecraft-router has no Ingress and no
URL, so the notes name the actual way in - the NodePort Service and the
fact that the routing decision is made on the hostname from the Minecraft
handshake - print the effective routing table, and state the traps: the
nodePort outside the default range, mappings as the only source of routes
(no Kubernetes auto-discovery in this chart), the restart a routing change
costs, that a Ready router says nothing about any backend, and that API
writes do not survive a restart. Conditional blocks for the out-of-range
nodePort, scale-to-zero, more than one replica and server.env.

No production domain anywhere; examples use example.com and
cluster-internal names. Minor bump, appVersion unchanged.
2026-08-29 00:32:25 +02:00
jklein fd7a653fa4 matrix-synapse: NOTES.txt and the exponential-quantity fix (7.1.0+upv1.159.0)
Two additive changes, neither of which alters a rendered object.

NOTES.txt (Teil von #43): prints what an operator would otherwise have to
read out of the templates - the rendered access URL or the port-forward
command, the field the 1Password item must carry (homeserver.yaml) plus the
four settings inside that file which this chart's mounts dictate, and the
chart's own traps: the split between matrix.delegation.serverUrl and
ingress.domain, the immutable server name, that a changed 1Password item does
not restart the StatefulSet while the delegation nginx does roll on a config
change, and that the PVC with signing key and media is deleted with the
release. Conditional on the values actually set: Ingress on/off, delegation
on/off, either workload scaled to zero, more than one Synapse replica, and a
matrix.env that is set. No real domain anywhere; the URLs render from the
values.

_helpers.tpl (Teil von #157): the canonical formatNumber helper, taken over
unchanged from template-chart. multiplyQuantity turned its input into a string
with printf "%v"; values that came back from fromYaml as float64 switched to
exponential notation from about 1e6 upwards, so a bare byte count like 1048576
became "1.048576e+06" and the render aborted. Both resource paths of this
chart were affected - matrix.resources in the StatefulSet and
matrix.delegation.resources in the delegation Deployment.

Minor bump, appVersion unchanged.
2026-08-29 00:31:40 +02:00
jklein bf02dab364 paperless-ngx: NOTES.txt and the #157 render fix (12.1.0+up3.0.5)
Two additive changes, no rendered Kubernetes object changes except the
resource limits that previously aborted the render.

#157 — a bare integer resource value from about 1e6 upwards aborted the
render. The null-safe fallbacks pass values through fromYaml, which parses
every number as float64, and Go's "%v" switches float64 to exponential
notation from that size on: 1048576 became "1.048576e+06", which is not a
valid Kubernetes quantity. The canonical correction from the issue is taken
over verbatim: multiplyQuantity formats its input and its non-integer product
through a new formatNumber helper instead of "%v". The toYaml/fromYaml round
trip is not touched — it was just unified across all charts and it is not the
cause.

This chart has two resource paths and both were affected: paperless.resources
(sfs) and smbConnection.cron.resources (consume CronJob). Measured on both
before and after, plus the 65536 fallback probe, which is unchanged.

#43 — NOTES.txt. Prints what an operator would otherwise read out of the
templates: the rendered URL and that ingress.domain also becomes PAPERLESS_URL,
the field names each 1Password item must carry (including that the database
field is "server" and that "username" is used as both admin login and admin
mail), and that secrets.oidcSecretRef is read by no template at all. The
conditional sections cover the SMB consume strecke, scaleDown, OIDC, Tika and
the barcode settings actually in effect. The traps it names are the ones this
chart really has: no checksum annotation anywhere, so rotating an item's
content restarts nothing; Redis failing silently while the UI stays green; the
600s startup budget against migrations and the 3.x index rebuild; the consume
PVC being hardcoded to 1Gi; the CronJob moving only top-level *.pdf and having
no probes (the documented exception); and a cron schedule starting with "*"
aborting the render.

README is deliberately left alone: the line listing the charts with a NOTES.txt
is pulled up once at the end of the programme, so that parallel NOTES PRs do
not collide on it.

Teil von #157
Teil von #43
2026-08-29 00:22:43 +02:00
jklein 65cb33e5f2 calibre-web-automated: add a NOTES.txt worth reading twice (6.1.0+upv4.0.6)
Prints what an operator would otherwise dig out of the templates: the rendered
access URL or the concrete port-forward, the fields the 1Password item has to
carry, and the traps of this chart in particular - the public default login,
the ingest folder that deletes what it imports, the fixed 1Gi ingest claim,
the one shared library claim, and why a change inside the 1Password item rolls
nothing (subPath mounts included).

Conditional on the values that actually decide something: ingress on/off,
scaled to zero, an unset secret ref, and extra env entries.

Part of #43.
2026-08-29 00:19:21 +02:00
jklein 5cc53b0772 calibre-web-automated: keep large integer quantities out of exponential notation
The null-safe fallback helpers from #99 pass values through fromYaml, which
parses every number as float64. Go's "%v" switches float64 to exponential
notation from about 1e6 upwards, so a bare byte count like 1048576 reached
multiplyQuantity as "1.048576e+06" and the render aborted on the suffix check.

Route both the helper's input and its non-integral product through a new
formatNumber helper, which forces decimal notation and trims the padding
zeros again. Strings such as "512Mi" pass through untouched.

Part of #157.
2026-08-29 00:19:13 +02:00
jklein 8189b74f55 rallly: NOTES.txt and the #157 number formatting (8.1.0+up4.13.0)
Two additive changes in one release.

multiplyQuantity turned a bare integer resource value of about 1e6 or more
into exponential notation and aborted the render with "unsupported resource
quantity suffix \"e+06\"". The null-safe helpers from #99 pass values through
fromYaml, which parses every number as float64, and Go's "%v" switches those
to exponential form. The new rallly.formatNumber helper forces plain decimal
notation on both the input and the non-integer product; it is the same wording
already merged into template-chart, zipline and voucher-vault.

The NOTES.txt prints what an operator would otherwise have to read out of the
templates: the rendered access URL or the port-forward command, the fields each
1Password item has to carry, the OIDC redirect URI, and the traps of this chart
- OIDC is the only way in because email login and registration are hard-coded
off, a rotated 1Password item does not restart the pods, the probes only prove
that the web process answers, and the Next.js cache emptyDir counts against the
container's ephemeral-storage limit.

Part of #157
Part of #43
2026-08-29 00:19:11 +02:00
jklein 17c2391895 outline: NOTES.txt and the exponential-quantity fix (7.1.0+up1.9.2)
Part of #157: multiplyQuantity turned a bare integer resource value into
exponential notation and aborted the render. The null-safe helpers pass
values through fromYaml, which makes every number a float64, and Go's "%v"
switches float64 to exponential form from about 1e6 upwards - so
"1048576" arrived as "1.048576e+06" and the suffix check failed. A new
outline.formatNumber helper renders such a value as a plain decimal
string; it is applied to the input and to the non-integer product. This is
the canonical fix agreed for the issue, taken over unchanged so the copies
of the helper in all charts stay identical.

Part of #43: the chart gets a NOTES.txt. It renders the access URL (or the
port-forward command when the Ingress is off), lists the fields each of the
three 1Password items has to carry plus the OIDC redirect URI, and states
the traps that are otherwise found the hard way: editing a value inside a
1Password item restarts nothing, Postgres and Redis are external and
unencrypted by default, migrations run inside the startup probe's budget,
uploads sit on a single RWO volume that helm uninstall takes with it, the
Ingress body-size limit is ten times smaller than the app's, and the chart
configures no mail at all.

Minor bump: the fix changes no rendered output for any value the schema
accepted before, and a NOTES.txt renders no object.
2026-08-29 00:10:42 +02:00
jklein db00198c6b zipline: NOTES.txt and the exponential-quantity fix (7.1.0+up4.7.0)
Part of #43 — templates/NOTES.txt.

The rule from the approved plan was that a NOTES.txt repeating the values is
worthless, so this one prints what an operator would otherwise look up in the
templates: the rendered URL (or the port-forward command when the Ingress is
off, plus the reason ingress.domain stays required — it is
CORE_DEFAULT_DOMAIN), the two op:// refs together with the exact field names
the 1Password items must carry ("connection-url" and "server-secret", because
the operator names the Secret keys after the item's fields), and the traps of
this chart: the setup page hands the super administrator to whoever reaches
the URL first, migrations run inside the startup probe's window (computed
from the effective probe values), a database outage leaves the pod running
and NotReady because liveness deliberately uses "/", the upload ceiling that
actually bites is the Ingress annotation and not the app's fixed 500mb, /tmp
is an emptyDir counted against the ephemeral-storage limit (computed) so an
oversized upload evicts the pod, and the uploads PVC is an ordinary release
resource that "helm uninstall" deletes. Conditional sections cover scaleDown
/ replicas: 0, more than one replica against the single ReadWriteOnce claim,
and a set zipline.env. No domain is baked into the chart; every host is
rendered from the values, examples use example.com.

Part of #157 — multiplyQuantity.

The null-safe fallbacks pass values through fromYaml, which parses every
number as float64, and Go's "%v" switches float64 to exponential notation
from about 1e6 upwards: a bare "1048576" arrived as "1.048576e+06" and the
render aborted on the suffix check. This is the canonical correction agreed
for all charts, taken over verbatim with this chart's prefix — the input and
the non-integer product now go through a formatNumber helper, and the
toYaml/fromYaml round trip is not touched, since it is not the cause.

Measured against ci/zipline/test-values.yaml on the chart's only resources
path, zipline.resources: before, both requests.memory=1048576 and
requests.ephemeralStorage=1048576 aborted with
  unsupported resource quantity suffix "e+06" in "1.048576e+06"
after, they render with the limits "3145728", while the 65536 fallback probe
still yields 196608 unchanged. Rendered manifests are byte-identical to the
previous version for the default values.
2026-08-29 00:10:31 +02:00
jklein e0541a0352 voucher-vault: add a NOTES.txt worth reading twice (6.1.0+up1.30.1)
The notes print what an operator would otherwise have to dig out of the
templates or out of the image, not a restatement of the values.

The centrepiece is the secret inventory: the three OnePasswordItem CRs
carry item paths only, so the field names each item must contain are
decided in this chart and appear nowhere in 1Password. A missing field
does not fail the install - the operator writes a Secret without that key
and the pod stays in CreateContainerConfigError.

The warnings are the ones this chart earns, all read out of the running
image rather than assumed:

- The first start against an empty database creates the superuser "admin"
  with a random password and prints it to stdout exactly once. With OIDC
  on, the app drops its password-change pages and the login form redirects
  to the provider by itself, so /admin/ is the documented way back in.
- The probes send Host: 127.0.0.1, which is always in ALLOWED_HOSTS. A
  wrong ingress.domain is therefore invisible: probes pass, the rollout
  goes green, and every real request gets 400.
- The entrypoint waits for Postgres in an nc loop with no timeout before
  opening the HTTP port, so a wrong database host produces no error at all
  - just a log that stops, until the startup probe budget runs out.
- Celery worker and beat are started detached from the same entrypoint;
  uwsgi answers whether or not they survived, so a wrong redis-url costs
  the expiry notifications silently.
- Every start runs makemigrations and migrate, and helm rollback does not
  reverse them.
- Expiry notifications need a per-user Apprise URL, and the beat schedule
  is created only while an empty database is initialized.

Conditional blocks cover the ingress and no-ingress access paths, the
scaled-down state, the storage class, and the case where voucherVault.env
has replaced the chart's full env list instead of extending it. The
startup budget and the replica state are read through the chart's own
helpers. No real domain appears anywhere; the one place an example is
needed uses example.com.

Minor bump, purely additive: a NOTES.txt changes no rendered object.

Part of #43
2026-08-29 00:03:06 +02:00
jklein 800a198017 voucher-vault: keep large integer quantities out of exponential notation
The null-safe fallback helpers pass values through fromYaml, which parses
every number as float64. Go's "%v" switches float64 to exponential
notation from about 1e6 upwards, so a bare byte count like 1048576 reached
multiplyQuantity as "1.048576e+06" and the render aborted with

  unsupported resource quantity suffix "e+06" in "1.048576e+06"

Both the input and the non-integer product now go through a formatNumber
helper that forces decimal notation and trims the padding zeros; strings
such as 512Mi pass through untouched. This is the canonical fix agreed for
all charts - multiplyQuantity is the same copy everywhere, so deviating
here would create drift instead of a solution.

Measured against ci/voucher-vault/test-values.yaml on both request keys
that get a computed limit (memory, ephemeral-storage): 1048576 renders a
limit of "3145728" instead of aborting, and the fallback case 65536 is
unchanged at 196608.

Part of #157
2026-08-29 00:02:51 +02:00
jklein 0217422179 template-chart: NOTES.txt and the naked-integer render fix (4.1.0+upxxx)
Two changes to the reference implementation, both of which every chart copied from it inherits.

Render fix (part of #157): multiplyQuantity turned its input into a string with printf "%v". The null-safe fallback helpers pass values through fromYaml, which parses every number as float64, and Go's "%v" switches float64 to exponential notation from about 1e6 upwards - so a bare byte count like 1048576 arrived as "1.048576e+06" and the suffix check aborted the render. A new xxx.formatNumber helper forces plain decimal notation on both the input and the non-integer product; strings like "512Mi" pass through untouched. This is the canonical fix agreed for the issue, taken over verbatim, because multiplyQuantity is the same copy in every chart and a private variant would create drift instead of a solution. The toYaml/fromYaml round-trip is deliberately not touched: it is not the cause.

NOTES.txt (part of #43): the scaffold now carries the reference NOTES.txt - a placeholder-carrying template, not finished prose. It renders the access URL from ingress.domain (no real domain anywhere), names which fields the 1Password item must carry, and prints the chart's traps: the placeholder image, the fact that both a Deployment and a StatefulSet render, and that no checksum/secret-ref means a secret change restarts nothing here. Output is conditional on the values actually set.

appVersion unchanged; minor bump because both changes are additive.
2026-08-28 23:59:38 +02:00
jklein 8df1913c5c bentopdf: new chart for the PDF toolbox (1.0.0+up2.8.7)
BentoPDF computes entirely in the visitor's browser; the container only serves
static files through nginx. The chart is therefore close to cyberchef - same
nginx-unprivileged base, uid 101, port 8080, probes on "/" - with four
differences that shaped the design.

Runtime config.json. The one setting reachable at runtime is the tool list.
It is a static file in the middle of the web root, so it is mounted by
subPath: a directory mount would hide the whole application. subPath mounts
are never refreshed by the kubelet, which makes the checksum/config annotation
(#82) load-bearing rather than decorative. bentopdf.config is passed through
verbatim, like searxng's settings, so a key upstream adds tomorrow is a values
change and not a chart change.

Three env switches that would kill the pod. PORT, DISABLE_IPV6 and
ROBOTS_NOINDEX are implemented as "sed -i" against files in the image, run
under "set -e" in /docker-entrypoint.d/, and the base image's entrypoint
aborts when one of them fails. With readOnlyRootFilesystem: true the pod
therefore never starts instead of ignoring the switch - measured in k3d as
CrashLoopBackOff, exit 1, "sed: can't create temp file
'/etc/nginx/nginx.confXXXXXX': Read-only file system". None of the three is
offered as a first-class value; values.yaml names the failure picture and the
escape hatch (env block plus readOnlyRootFilesystem: false).

Writable paths were measured, not copied from the upstream chart.
/etc/nginx/tmp holds the pid file and /var/cache/nginx is where nginx creates
its temp directories at startup - without it the master aborts with
'mkdir() "/var/cache/nginx/client_temp" failed'. /var/run is deliberately not
mounted: the running pod proves it is unnecessary.

Everything else about BentoPDF is a build-time argument and out of reach for
any chart; values.yaml says so explicitly rather than leaving it to be
discovered. Two accepted consequences are named there too: some tools fetch
WASM modules from a public CDN at runtime, and the served pages carry a
canonical link to the vendor's site.

Fixes #174
2026-08-28 18:22:30 +02:00
jklein cf43d90beb searxng: state the limiter probe measurement instead of hedging it
The comment on the probe block said a probe on "/" would "risk" a 429 once
the limiter is on. It was measured against a reachable Valkey in k3d and it
does not risk it, it collects one: "/" answered 429 while /healthz answered
200. Comment only, no rendered change.
2026-08-27 21:13:08 +02:00
jklein 46942db53a searxng: new chart (1.0.0+up2026.8.22.9fea41204)
SearXNG as a stateless Deployment plus a settings ConfigMap, with the four CI
steps a new chart needs: publish workflow, change-app-version choice list,
ci/searxng/ fixtures and a renovate.json5 block.

Three points where the chart is not a cyberchef with another name:

* The image entrypoint writes settings.yml into /etc/searxng only when the
  file is missing. The chart always mounts one by subPath, so the copy and
  the random-secret sed never run and the container needs no write access
  there - which is what allows readOnlyRootFilesystem: true.
* The signing key is a required OnePasswordItem, not a ConfigMap key. It
  signs the settings cookie and the image-proxy URLs, and the entrypoint's
  self-generating fallback cannot fire here, so without it the instance would
  run on the literal "ultrasecretkey" - which webapp.py refuses with exit 1.
* All three probes use /healthz, the only path the bot detection lets through
  unconditionally. Measured: with the limiter active, "/" answers 429 while
  /healthz still answers 200.

ingress.domain is read a second time outside the Ingress template, for
SEARXNG_BASE_URL, and carries its own required() there - so it stays
mandatory with the Ingress switched off.

The Renovate versioning regex deviates from the plan on the issue and matches
the commit hash WITHOUT capturing it. Renovate parses a "build" group with
Number.parseInt, so a hash becomes the digits leading it; measured on
renovate 41.173.1 and 44.48.0, the planned form reports an image pushed
earlier the same day as an update to a newer one. Reasoning in the block and
in README.
2026-08-27 21:09:59 +02:00
jklein e5f98435ed ci: redact declared secret values from install-test diagnostics
The failure path prints container logs, and an app may print a value it was
given - gluetun logs its Wireguard address on startup. diagnose() now runs
through a filter that replaces the values of the declared REQUIRED_ENV
variables with the variable name. GitHub masks them as well, but that is the
platform's promise rather than this harness's, and the script runs locally too,
where nothing masks anything.
2026-08-26 14:37:46 +02:00
jklein 69a280399c ci: run the freesailarr install-test for real instead of skipping it
freesailarr was excluded from the install-test because gluetun never becomes
healthy without a real Wireguard key and no app container behind its kill
switch can become Ready. The green "Install-test freesailarr pass" was a
25-second SKIP on the most complex chart in the repo - a signal that proved
nothing.

With a CI-only Mullvad key in 1Password the reason is gone:

- ci/run-install-test.sh learns a general rule: ci/<chart>/fixtures.env.yaml is
  rendered with envsubst before it is applied, restricted to the variable names
  a chart declares in REQUIRED_ENV. A missing or empty declared variable fails
  the run loudly and names the variable - it never skips the chart.
- The install-test job loads the key from 1Password Connect for freesailarr
  only, following the pattern publish-helm-chart.yml documents: values via
  steps.<id>.outputs.*, mapped into the step's own env, quoted, with ":?"
  guards that name the missing op:// field before a cluster is created.
- Fixtures for freesailarr: three media claims, the gluetun secret template
  with variable names only, firewallOutboundSubnets so the kill switch does not
  block cluster DNS, and INSTALL_TIMEOUT=900 for nine containers.

freesailarr/Chart.yaml goes 3.0.0+up1.0.0 -> 3.0.1+up1.0.0. That bump is
deliberately empty of content: the install-test matrix follows changed chart
directories, and without it this PR could not run the test it adds.
2026-08-26 14:30:42 +02:00
jklein 65c35dff46 freesailarr: two measured details from the k3d end-to-end run
Both are documentation only - the rendering is byte-identical to the previous
commit.

The *arr probes: with a UrlBase set, radarr/sonarr/prowlarr answer the bare
/ping with 307 and Location: /<urlbase>/ping, not 200. The probes keep passing
because the kubelet counts 200-399 as success (9/9 Ready, 0 restarts), so the
probe paths stay at /ping and are valid in both ingress shapes. Recorded so
nobody reads the 307 as a fault.

Bazarr under a sub-path without its manual base URL: it does not answer 404.
It returns its index page for every unknown path, so /bazarr/ and
/bazarr/<asset> both come back as 200 text/html of identical length - the page
loads, the assets silently arrive as HTML, and nothing looks like an error.
The content type is the tell, not the status code.
2026-08-25 21:10:49 +02:00
jklein fc768efaf5 freesailarr: shared domain with sub-paths, qBittorrent save path (3.0.0+up1.0.0)
Adds a second, optional ingress shape and fixes the download target of
qBittorrent. The existing per-app-host shape stays the default and renders
unchanged.

Shared domain (#167)

Setting ingress.host moves every app without a host of its own into one
Ingress object with one path each (<app>.ingress.path, default /<app>), one
TLS secret and therefore one DNS name and one certificate for the whole
stack. A per-app host always wins and keeps its own object, so mixed
operation needs no extra values.

The apps differ in what they can do under a sub-path, and the chart follows
that rather than pretending they are alike:

  - radarr, sonarr, prowlarr get <APP>__SERVER__URLBASE from the chart, but
    only when their own env block does not already carry the variable.
  - bazarr has no environment variable for its base path and stays one
    documented manual step (Settings -> General -> Base URL).
  - qbittorrent has no base path at all and renders a second object on the
    same host with a prefix-stripping rewrite. It repeats the same TLS secret
    name and deliberately omits the cert-manager annotation, so both objects
    state their own TLS while only one certificate is requested.
  - seerr has no base path either and cannot be rewritten, so it is pinned to
    "/" and a fail refuses any other path. A second fail refuses two apps
    claiming the same path.

qBittorrent save path (#166)

Without Session\DefaultSavePath qBittorrent writes to its built-in default
/config/Downloads - the 1Gi config volume. It fills within minutes, every
torrent goes to "errored" and qBittorrent can no longer write its own
metadata to BT_backup, so a restart in that state costs the torrent list.

The init container (renamed qbittorrent-config-interface ->
qbittorrent-config-seed, since it no longer only sets the interface) now
seeds qbittorrent.savePath, default /downloads. Unlike the interface binding
it is inserted ONLY when the key is absent and never replaced: a path changed
in the WebUI is a legitimate operator decision and must survive a restart.

This is the reason for the major bump: an installation that never set the
path writes somewhere else after the upgrade.

Documentation

NOTES.txt and the media block in values.yaml now spell out why a shared claim
with three subPaths does not produce hardlinks: the kernel compares the
vfsmount in do_linkat, not the superblock, so two bind mounts of one
filesystem are EXDEV - while stat reports the same device number on both
sides and suggests the opposite. The layout that would work (one claim, one
mount, categories as sub-directories) is named as a possibility; the chart
cannot express it today.

README.md gains the shared domain as the second admissible ingress shape of
the style guideline.
2026-08-25 20:55:43 +02:00
jklein 39e9613ee3 freesailarr: one volume entry per media claim (2.0.2+up1.0.0)
media.downloads, media.movies and media.tv rendered three fixed volume
entries, one per category. Two categories on the same existingClaim
therefore produced two pod volumes with an identical claimName, and such a
pod never starts: it stays in Init:0/1 without a single kubelet event
(measured on RKE2/Longhorn, RWO and RWX alike).

The volume list is now deduplicated by claim name. A new mediaVolumeName
helper names the entry after the first ENABLED category in the fixed order
downloads, movies, tv that carries the claim; the other categories mount
that same entry and differ only in their subPath. The restriction to
enabled categories matters because the three entries render under
different conditions - a canonical owner whose entry is not rendered would
be a dangling reference.

With three distinct claims the chart renders byte-identically to 2.0.1,
hence a patch bump.

Also corrected in the values documentation, after measuring instead of
assuming: the kubelet creates missing subPath directories itself, but they
inherit the claim root's mode, and fsGroup cannot be relied on to fix that
- Longhorn's fsGroupPolicy (ReadWriteOnceWithFSType) skips fsGroup on RWX
claims entirely. What has to hold is that the claim root is writable for
PUID/PGID. No chown or init container was added.
2026-08-24 21:01:53 +02:00
jklein 8a68df737a minecraft-server: restore the java21 line (4.0.1+up2026.8.2.java21)
Closes the three-step round that carried the #99 null-safety to every Java
line. Pure restoration of the directory to the newest line:

  version:            4.0.0+up2026.8.2.java17 -> 4.0.1+up2026.8.2.java21
  appVersion:         "2026.8.2-java17"       -> "2026.8.2-java21"
  minecraft.version:  "1.20.1"                -> "1.21.1"

Nothing changes in content, and that is not a claim but a measurement. Against
the java21 state published from #156, the ENTIRE chart tree differs in exactly
one line - the chart version - and .Chart.Version is not read by any template,
so the rendering is byte-identical:

  4.0.0+up2026.8.2.java21 (published) -> this chart:
    file tree: 1 differing line, Chart.yaml version
    rendering: byte-identical (CI values, serverpack init container enabled)

  main (java17) -> this chart:
    exactly TWO differences, both the line selection itself -
    image itzg/minecraft-server:2026.8.2-java17 -> :2026.8.2-java21 and
    env VERSION 1.20.1 -> 1.21.1.

Why 4.0.1 and not 4.0.0: 4.0.0+up2026.8.2.java21 is already in the registry
from #156, and helm push overwrites an existing OCI tag without complaint. The
version check in validate-charts.yml does not catch this - it compares only
against github.base_ref, where the version would read java17, so a republished
4.0.0 would pass the gate and silently overwrite the published artifact. Tracked
as #160.

What this round achieves, measured across the three published line states:
templates/ and values.schema.json are byte-identical on java8 (f7047b8), java17
(2b22ce5) and java21 (this chart); values.yaml differs only in minecraft.version
and the comment naming the appVersion; and all three now carry initImage
alpine:3.24, so the bump that had reached only java21 is no longer missing on
the other two. All three lines therefore run the same hardening, the same three
exec probes, the same resources handling and the same null-safe fallbacks.

Left untouched on purpose: the float64 quantity finding (#157) and seccompProfile
(#161) - the latter has to be pulled through all three lines and has its own
unknowns in gosu and the serverpack init container.

Refs #99
2026-08-24 17:26:41 +02:00
jklein 73b304c1aa minecraft-server: publish the java17 line (4.0.0+up2026.8.2.java17)
Third of three line publications that carry the #99 null-safety to every Java
line. Templates and schema are the same for all three lines; only the
line-selecting fields change:

  version:            4.0.0+up2026.8.2.java8 -> 4.0.0+up2026.8.2.java17
  appVersion:         "2026.8.2-java8"       -> "2026.8.2-java17"
  minecraft.version:  "1.12.2"               -> "1.20.1"   (Java 17 runs
                                                            Minecraft 1.18-1.20.4)

What the diff does NOT show, same as for java8: the java17 line was last
published as 3.0.0+up2026.8.2.java17, and since then main also moved the
serverpack init image from alpine:3.22 to alpine:3.24 (commit 5a35ea4, which
landed on the java21 line only). That bump rides along here.

It is invisible in the diff against main, because relative to main the file
already says 3.24 - the java8 publication in #159 brought it there. Relative to
what a java17 release actually runs today it is a base-image change, so it is
named here rather than left to be discovered.

Measured against ci/minecraft-server/test-values.yaml with the serverpack init
container enabled, compared structurally (documents paired by kind/name,
recursive over keys) instead of line by line:

  3.0.0+up2026.8.2.java17 (published) -> this chart:
    exactly ONE difference, initContainers[0].image alpine:3.22 -> alpine:3.24.

  main (java8) -> this chart:
    exactly TWO differences, both the line selection itself -
    image itzg/minecraft-server:2026.8.2-java8 -> :2026.8.2-java17 and
    env VERSION 1.12.2 -> 1.20.1.

Hardening, the three exec probes, resources and the init.sh chown are unchanged,
which is what the null-safety was built to do: it changes what an ERASED block
renders, not what the defaults render.

At file level the line invariant holds exactly - against main's tree the only
differing files are Chart.yaml and values.yaml, in the three line fields plus
the comment naming the appVersion. _helpers.tpl, values.schema.json and both
templates are byte-identical across the lines.

The null-safety itself is not re-swept - #156 proved it on identical templates.
Confirmed once on this build that it carries: erasing both security contexts
renders byte-identical to the defaults.

Left untouched on purpose: the float64 quantity finding (#157), because
multiplyQuantity is the same copy in all 21 charts and a single-chart fix would
create drift, and seccompProfile (#84), which changes runtime behaviour and
belongs in its own change.

Refs #99
2026-08-24 17:19:50 +02:00
jklein b049101da5 minecraft-server: publish the java8 line (4.0.0+up2026.8.2.java8)
Second of three line publications that carry the #99 null-safety to every Java
line. The chart is published once per line, the templates and the schema are
the same for all three, and only the directory's line-selecting fields change:

  version:            4.0.0+up2026.8.2.java21 -> 4.0.0+up2026.8.2.java8
  appVersion:         "2026.8.2-java21"       -> "2026.8.2-java8"
  minecraft.version:  "1.21.1"                -> "1.12.2"   (Java 8 runs
                                                             Minecraft <= 1.16.5)

What the diff does NOT show, and what this commit therefore has to say out
loud: the java8 line was last published as 3.0.0+up2026.8.2.java8, and since
then main also moved the serverpack init image from alpine:3.22 to alpine:3.24
(commit 5a35ea4, which landed on the java21 line only). That bump rides along
here. It is invisible in this PR's diff, because relative to main the file
already says 3.24 - but relative to what a java8 release actually runs today it
is a base-image change, and the cluster side has to know.

Measured, rather than assumed. Rendered against ci/minecraft-server/test-values
with the serverpack init container enabled and compared structurally (documents
paired by kind/name, recursive over keys) instead of line by line:

  3.0.0+up2026.8.2.java8 (published) -> this chart:
    exactly ONE difference, initContainers[0].image alpine:3.22 -> alpine:3.24.

  main (java21) -> this chart:
    exactly TWO differences, both the line selection itself -
    image itzg/minecraft-server:2026.8.2-java21 -> :2026.8.2-java8 and
    env VERSION 1.21.1 -> 1.12.2.

Nothing else moves: the hardening, the three exec probes, the resources and the
init.sh chown are unchanged, which is what the null-safety was built to do (it
changes what an ERASED block renders, not what the defaults render).

The null-safety itself is not re-swept here - #156 proved it on identical
templates. Confirmed once on this build that it carries: erasing both security
contexts renders byte-identical to the defaults.

The float64 quantity finding (#157) is deliberately untouched, per its own
issue: multiplyQuantity is the same copy in all 21 charts and a single-chart
fix would create drift.

Refs #99
2026-08-24 17:11:12 +02:00
jklein fc8f57e176 minecraft-server: null-safe hardening, probes and resources
The last open gap of #99. minecraft-server was the one chart of 21 where an
erased security context removed the hardening in silence: it never got the
defaultedDict helper, the StatefulSet rendered .Values.securityContext.pod
straight through toYaml, and the schema typed securityContext.pod as a strict
"object" without required. Measured against ci/minecraft-server/test-values.yaml
on 3.0.1:

  --set securityContext.pod=null --set securityContext.container=null
  -> helm exit 0, no schema error, "securityContext: null" for BOTH the pod and
     the container - no runAsNonRoot, no readOnlyRootFilesystem, no dropped
     capabilities, in a chart with a PVC.

Bitter place for it: the README lists this chart as the pilot for #68. It got
its schema early and was therefore never caught by the later null-safety wave.

Both halves are added, because they cover different cases. For a key WITH a
chart default Helm's coalescing deletes the null before validation ever sees
it, and only the defaultedDict fallback catches that; for a key WITHOUT one
(server.resources.limits) the null survives into validation, where the strict
"object" aborted the render. Now widened to ["object","null"].

The fallback table reproduces THIS chart's defaults, deviations included:
uid/gid pinned to 1000 (the image's minecraft user - starting as it directly
takes the entrypoint's non-root branch, which skips the recursive chown of
/data and the write to /etc/nsswitch.conf that a read-only root filesystem
would reject), fsGroupChangePolicy OnRootMismatch (a recursive chown of a 32Gi
world on every start costs minutes), the explicit empty capabilities.add list,
and all three probes as exec probes running the image's mc-health script - this
image has no HTTP endpoint, so a probe default rebuilt as httpGet would leave
the workload with no working probe at all. The startup budget stays 15s x 80.

server.resources goes through the same helper while keeping its memory request
deliberately unset, so it is still derived from the JVM heap.

Also, the same class one level down: init.sh chowned the migrated serverpack to
a hardcoded 1000:1000 while the uid it has to match was overridable. Both reads
now go through the same helper, so they cannot disagree. With the chart defaults
the rendered script is unchanged, so checksum/config does not move and no
existing pod rolls.

minecraft.resources.storage and .memory stay deliberately not null-safe: an
erased value yields an invalid PVC or an unparseable quantity, which is the loud
class #99 excludes.

Evidence, all against ci/minecraft-server/test-values.yaml unless noted:

  - the reported call now renders the defaults, byte-identical to the
    unmodified rendering. Fallback, not rejection.
  - rendering with securityContext, all three probes and server.resources set
    to null is byte-identical to rendering the chart defaults - which is what
    keeps the table and values.yaml in sync instead of leaving it to discipline.
  - no regression: rendering before and after the change is byte-identical, on
    the CI values, on the chart defaults, and with curseforgeServerpack enabled
    (the init-container path the CI values do not exercise). Confirmed
    structurally too, not only by raw diff.
  - partial nulling, 14 keys swept one at a time. On 3.0.1 twelve of them
    changed the rendering silently, including capabilities (the container kept
    every default capability instead of dropping ALL) and requests (cpu and
    ephemeral-storage disappeared entirely). On 4.0.0 all fourteen render
    identical to the defaults.
  - explicit false and 0 still win over the defaults; a scalar in a mapping slot
    is still rejected, now naming the values path.

Major bump: erasing one of these blocks used to change the rendered workload
and now does not, which is a runtime behaviour change. appVersion is untouched.

Refs #99
2026-08-24 16:53:25 +02:00
jklein d9c7878afe README: bound the icon exception to its criterion
The previous wording named freesailarr as a documented exception but closed on
"the field is omitted, lint still exits 0", which reads as a general licence to
drop the icon. The rule is now stated with its criterion and its reach: the
exception exists because the chart has no single upstream whose logo the icon
could be, it is the only chart in the repo without one, and a single-upstream
chart missing an icon is a missing icon rather than an exception.
2026-08-24 16:27:53 +02:00
jklein a37588c2fe freesailarr: schema, null-safe fallbacks, seccompProfile, NOTES.txt
Retrofits the repo-wide groundwork that #59 deliberately left out to keep the
reviewed scope. Chart 2.0.0+up1.0.0 - a major, because seccompProfile changes
runtime behaviour; appVersion stays the freely chosen marker 1.0.0.

values.schema.json (#68). The largest schema in the repo: nine app blocks with
probes, resources, security contexts, env and image. additionalProperties is
false on every chart-owned level; the objects that go verbatim into the pod
spec stay open. It rejects a mistyped app name, a mistyped key at any depth and
ingress.domain - the key #59 removed, which until now would have been accepted
in silence.

Null safety (#99), both halves, because they cover different cases. The
defaultedDict helper catches a key WITH a chart default, where Helm's
coalescing deletes the null before the schema ever sees it; the widened
["object","null"] types catch a key WITHOUT one, where the null survives into
validation. Measured on the old chart: radarr.securityContext: null rendered
"securityContext: null" - the whole hardening gone - and
securityContext.capabilities: null dropped the capability block entirely, so
the container kept every default capability instead of dropping ALL. Both
render green.

The fallback table reproduces THIS chart's defaults including its deviations:
the five linuxserver.io containers keep runAsNonRoot unset and a writable root
filesystem so their s6 init can drop to PUID/PGID, flaresolverr keeps its
numerically pinned uid, gluetun keeps NET_ADMIN, and recyclarr keeps its exec
probes. Restoring the "clean" standard there would put the linuxserver
containers back on the image's uid 911 - the failure calibre-web-automated hit.
The sync between table and values.yaml is not left to discipline: rendering
with every block of the silent class explicitly set to null is byte-identical
to rendering the defaults, which is how a drift shows up.

seccompProfile RuntimeDefault (#84) in the pod context, so it covers the init
container too. Trivy's KSV-0104 drops from ten findings to none.

NOTES.txt (#43) covering what actually bites: the pod is not Ready until the
tunnel is up, the two fields the 1Password item needs, the warning when
firewallOutboundSubnets is unset, the immutable volumeClaimTemplates, and the
measured fact that imports do not hardlink across the media categories.

Also, from point 5 of the plan: checksum/config does not apply - the chart has
no ConfigMap - but checksum/secret-ref does, and was missing. Helm never sees
the secret's contents, yet it does see which op:// item the OnePasswordItem
points at, so repointing secrets.gluetunSecretRef now restarts the pod instead
of leaving the old credentials running.

README: the #43 status line was stale (it still claimed only homeassistant
ships NOTES.txt), and the mandatory-icon rule now records freesailarr as a
documented exception - #59 dropped the icon and did not amend the rule.
2026-08-24 16:06:20 +02:00
jklein f5da9ce126 renovate: exclude the two legacy qbittorrent tags
linuxserver/qbittorrent still carries 14.3.9 and 20.04.1 from earlier tagging
schemes. Docker versioning sorts both above every real release, so Renovate
offers them as major updates - PRs #152 and #153 are exactly those, and they
would have to be closed by hand again after every lookup.

They cost more than the noise. Renovate fills the major bucket once, so while a
stray occupies it the genuine major is never offered at all: measured from
currentValue 4.6.4, the unconstrained lookup returns 4.6.7 and 20.04.1, and
never mentions 5.2.3.

Both are excluded by name. A bound such as "<10" would reject a genuine major
sooner or later, whereas two finished artifacts cannot grow and so cannot mask
a release that does not exist yet. A shape-based filter was tried first and
measured out: requiring a non-padded minor drops 20.04.1 and simply promotes
14.3.9 into its place.

Verified with renovate 44.41.0 (dry-run=lookup) against the live registry:
  currentValue 5.2.1 -> [5.2.3 (patch)], both strays gone
  currentValue 4.6.4 -> [4.6.7 (patch), 5.2.3 (major)]
The second fixture is the guard the nginx rule taught us to run - it proves the
filter does not silently empty the update list. A full run over the real
repository with the real config confirms it in place: qbittorrent offers 5.2.3
only, nginx is unchanged, and no docker dependency carries a skipReason or a
warning.
2026-08-24 15:45:57 +02:00
jklein 020e54d1ff Write the busybox image without the library/ namespace
docker.io/busybox and docker.io/library/busybox resolve to the same image on
Docker Hub, and the short form is the spelling the rest of the fleet will use
when the remaining unqualified official images (paperless-ngx, matrix-synapse,
minecraft-server) are qualified in the #84 follow-up.

Verified: the rendered output differs in exactly this one line, and a Renovate
lookup against the real chart files (44.41.0, official container, dry-run
lookup) still extracts all ten images through helm-values with no skipReason
and no warnings - docker.io/busybox included, whose candidate list stays empty
because 1.38.0 is current.
2026-08-24 15:37:10 +02:00
jklein 9cdbe097f6 Qualify the Docker Hub image names, drop the gluetun branding
main qualified five charts' image references in 6c73ef8 on the same day this
chart would otherwise arrive carrying the pattern that was just cleared out.
The seven Docker Hub names now say where they come from: qmcgaw/gluetun, the
five linuxserver.io images and the busybox init image, the last as
docker.io/library/busybox since it is an official image. The three ghcr.io
names were already qualified - checked rather than assumed.

Verified with Renovate 44.41.0 in the official container (RENOVATE_PLATFORM=
local, RENOVATE_DRY_RUN=lookup) that the prefixed form still resolves under the
built-in helm-values manager, since that manager is the only thing keeping
these images updated: it extracts all ten, datasource docker, no skipReason and
no warnings, and returns concrete candidates for eight of them. A control run
against the unqualified form produced byte-identical results, so the prefix
changes nothing about what Renovate offers.

Trivy's trusted-registry check (KSV-0125) drops from ten findings to none, which
is what the registry list in ci/trivy/data/ksv0125.yaml documents as the point
of writing images this way.

Chart metadata: the icon was gluetun's logo and the description opened with a
list of four of the nine projects, both suggesting a "main app" the stack does
not have. The description now names none of them, and the icon field is omitted
rather than filled with another project's logo - every other chart's icon is its
one upstream's, and this chart has no such upstream. helm lint --strict notes
"icon is recommended" as INFO and still exits 0.

Rendered output differs in exactly the seven image prefixes and nothing else.
2026-08-24 15:23:35 +02:00
jklein 09fe3c2be6 Decouple appVersion from gluetun in wording and value
The previous commit moved every image tag into values.yaml but left appVersion
described as a marker "for the gluetun line", to be updated together with
gluetun.image.tag. That still asserts a coupling the chart does not have, and
the value v3.41.1 confirmed it at a glance.

appVersion is now 1.0.0 (version 1.0.0+up1.0.0): the chart's own version
marker, freely chosen and tied to no upstream. The texts say that instead of
the old coupling in Chart.yaml, values.yaml and the two README passages. The
README's remaining true statement is kept but generalized - renovate-bump-chart
bumps the patch level on an unchanged appVersion for all nine images alike,
which is not a gluetun special case.

Also corrected on the way: the busybox init image comment claimed it was
"pinned independently of appVersion (which tracks gluetun)". Neither half holds
any more - appVersion tracks nothing, and every image in the chart is pinned
the same way.

No functional change: the rendered gluetun image is qmcgaw/gluetun:v3.41.1
before and after.
2026-08-24 15:10:14 +02:00
jklein af5f9ed79b Pin gluetun in values too, so Renovate proposes it there
The chart bundles nine independently versioned upstreams, and eight of them
were already pinned in values.yaml while only gluetun took its tag from
appVersion. That split had no upside: it made gluetun the one image Renovate
could not reach through the built-in helm-values manager, and it implied a
"main app" the stack does not have.

gluetun now carries gluetun.image.repository/tag like every other image, and
the StatefulSet renders it from there. No tag comes from appVersion any more.
Renovate picks all nine up through helm-values - the manager the repo already
relies on for sidecar images - and ci/renovate-bump-chart.sh bumps the chart
version with each update.

Consequences, all documented where they bite:
- renovate.json5 deliberately gets no app-version block for this chart. One
  would either duplicate the helm-values updates or rewrite an appVersion that
  selects no image. The absence is commented so it does not read as an
  oversight.
- freesailarr leaves the change-app-version.yml choice list. That workflow
  rewrites appVersion and republishes, which for this chart would change no
  deployed image at all.
- appVersion becomes a hand-maintained marker for the gluetun line. Since
  renovate-bump-chart.sh bumps the patch level on an unchanged appVersion, a
  gluetun update has to be paired with a hand edit of appVersion, or the +up
  suffix stops describing what the chart deploys. Noted in Chart.yaml,
  values.yaml and README.
2026-08-24 15:01:34 +02:00
jklein e4f70bc856 Correct two firewall claims the runtime test disproved
FIREWALL_INPUT_PORTS does not open the app ports inside a pod network. gluetun
detects the pod's own subnet and inserts an INPUT accept for it ahead of the
per-port rules, and every packet arriving at the pod IP matches that rule by
destination. Measured with the packet counters: 50 fresh requests moved the
subnet rule from 306 to 356 packets while every --dport rule stayed at 0 since
container start. A port that listens but is not in the list (gluetun's control
server on 8000) is reachable from another pod all the same.

The variable is kept - it is the explicit statement of which ports are meant to
be reachable, and the subnet detection is gluetun's behaviour rather than a
guarantee of this chart - but the comments no longer claim the traffic would be
dropped without it.

Second correction, same run: the /tmp/gluetun emptyDir fixed the "open ...
permission denied" failure, but gluetun also chowns the file, which needs
CAP_CHOWN. With all capabilities but NET_ADMIN dropped that logs "chown
/tmp/gluetun/ip: operation not permitted" on every VPN restart. The file is
written and the public IP is persisted, so this is a log line rather than a
failure, and granting CHOWN to silence it would widen the container's
capabilities for nothing. Noted where the mount is declared.
2026-08-24 14:38:03 +02:00
jklein bc6577f321 Document what the media subPaths actually buy
The runtime test contradicted the assumption behind the change. Kubernetes
turns every volumeMount into its own bind mount, and link()/rename() across two
mount points fails with EXDEV even when both sides are the same claim on the
same device: in the radarr container /downloads and /movies both report device
65025 and resolve to the same PV directory, yet linking between them fails with
"Cross-device link", while the identical link succeeds when the claim root is
mounted once and the categories are sub-directories under it.

It is not a property of subPath - two plain mounts of the same claim without
any subPath fail the same way, so the chart never supported cross-category
hardlinks and this change does not regress anything. But the comment claimed a
benefit the chart does not deliver, so it now states the measurement instead:
subPath buys the layout (one claim, one sub-directory per category), not
hardlinked imports. Getting those needs a single mount containing all
categories, which is a change to the mount layout rather than a values setting.
2026-08-24 14:08:24 +02:00
jklein 0fb6c30466 publish: read registry credentials from step outputs and validate them
Two defects kept the publish broken after the load-secrets-action v5 bump
in #140.

v5 no longer exports the loaded values as environment variables for later
steps the way v4 did; only the step outputs carry them. Every
"${{ env.JK_CR_* }}" reference therefore resolved to an empty string from
that bump on, and nothing reported it - the loading step logs "Populating
variable" and stays green either way. #149 moved three of the four
references to steps.<id>.outputs but left "-u ${{ env.JK_CR_USERNAME }}"
on the old form, and piped the username instead of the token into
--password-stdin.

The second defect is why this surfaced as a terminal error: an empty value
collapsed into a missing argument instead of failing. With an empty
username, "-u" swallowed the following "--password-stdin", helm stopped
reading the password from stdin, prompted interactively and died with
"inappropriate ioctl for device".

All four values now come from steps.<id>.outputs, are mapped into each
step's own env block and used as quoted shell variables guarded with ":?".
An empty or unset value aborts the job naming the op:// path to fix,
quoting keeps it from shifting arguments, and the token no longer appears
on the command line.
2026-08-24 14:03:09 +02:00
jklein 70c7106fe8 Give the Trivy scan render values for freesailarr
ci/run-trivy-config.sh renders every changed chart before scanning it and exits
non-zero when the render fails - which it does for freesailarr, because the
chart is excluded from the install-test and therefore has no
ci/freesailarr/test-values.yaml to fall back on. The scan job would go red
without ever scanning anything.

The file supplies only the values the chart declares required (secret ref,
cluster issuer, the three media claims and one ingress host per app, since the
chart has no global ingress.domain) and nothing that would alter the security
posture the scan is meant to look at.
2026-08-24 13:51:29 +02:00
jklein f6f04d27fe fix(publish-action): fixed 1Password secret access 2026-08-24 13:47:01 +02:00
jklein cdb66a32f4 Per-app ingress and service, media subPaths
Three review changes to the freesailarr chart.

Ingress: the chart no longer derives hosts as <app>.<ingress.domain> from one
Ingress object. The global ingress.domain is gone; every app carries its own
<app>.ingress.host and renders its own Ingress <release>-<chart>-<app>-ingress
with its own TLS secret tls-<release>-<chart>-<app>-ingress. The hosts are
therefore independent and may live in unrelated domains. The host is required,
but the required() call sits inside the loop over the *enabled* app ingresses,
so switching an app's ingress off must not make the chart demand a host for it.
clusterIssuer, className and annotations stay global defaults and are
overridable per app; an explicitly set per-app value wins (nil check instead of
"default", so an empty override is honoured).

Media: media.<category>.subPath mounts a sub-directory of the claim instead of
its root, default null (claim root = previous behaviour). The same
existingClaim may back all three categories with different subPaths. That is
the point rather than cosmetics: Radarr and Sonarr import by moving, and a move
within one filesystem can be a hardlink instead of a copy - across volume
boundaries every import duplicates the file.

Services: the collective Service with named ports is replaced by one ClusterIP
Service <release>-<chart>-<app>-service per enabled app that listens on a port,
flaresolverr included; recyclarr serves no port in daemon mode and gets none.
Each ingress points at its own app's service. All containers share gluetun's
network namespace, so the services differ only in their port - they provide
stable per-app DNS names, not network separation. The StatefulSet's governing
serviceName no longer names a removed object.
2026-08-24 13:45:38 +02:00
jklein d4f03cdfd9 Give gluetun a writable /tmp/gluetun
gluetun logs "clearing public IP data: open /tmp/gluetun/ip: permission
denied" every few seconds and never persists its public IP. It creates
/tmp/gluetun with mode 0644, i.e. without the execute bit needed to enter
the directory; it runs as root, but with all capabilities except
NET_ADMIN dropped there is no DAC_OVERRIDE left to paper over the missing
bit. An emptyDir provides the path with usable permissions.
2026-08-24 13:41:47 +02:00
jklein adc87c2cc1 Bind the qBittorrent session to the VPN interface
qBittorrent had no network interface configured, so libtorrent listened
and announced on every interface of the shared pod network namespace -
including eth0 (the pod IP) and loopback. gluetun's kill switch drops
exactly those packets, so trackers answered with "skipping tracker
announce (unreachable)" or timed out, no peers were found and torrents
stalled at 0 bytes in stalledDL. Beyond the stall this is a leak: without
the binding qBittorrent also listens on the pod IP, i.e. reachable past
the tunnel.

The setting only exists in the persisted config file, which qBittorrent
creates itself on first start, so an init container seeds it before the
app comes up. The edit is idempotent and preserves existing user
settings: it replaces the keys if present, inserts them into the
[BitTorrent] section if missing, and creates file or section if needed.
The interface is configurable via qbittorrent.networkInterface (default
tun0); emptying it drops the init container.
2026-08-24 13:41:47 +02:00
jklein 5e2d8e4083 Allow writable root filesystem for the linuxserver.io containers
With readOnlyRootFilesystem the s6-overlay init cannot rewrite
/etc/passwd, so the containers silently kept the image's built-in
uid/gid 911 instead of the configured PUID/PGID (verified at runtime) -
breaking ownership on the shared media volumes. Drop the now-unneeded
/run and /tmp emptyDirs; deviation documented in values.yaml.
2026-08-24 13:41:47 +02:00
jklein 265190d175 Calibrate gluetun liveness more tolerant than readiness
Short tunnel re-negotiations recover internally; a kubelet restart only
prolongs the outage while the kill switch already blocks all egress.
Liveness budget 60s, readiness stays at 30s.
2026-08-24 13:41:47 +02:00
jklein 0a5035ae83 Clean stale VPN state synchronously before gluetun starts
Replace the upstream-recommended postStart hook, which runs
concurrently with the entrypoint and loses the race (observed: it
deleted the ip rule the restarted gluetun had just created, leaving the
container in a permanent CrashLoop behind the kill switch). A command
wrapper now removes stale table-51820 rules and the leftover tun
device deterministically before exec'ing /gluetun-entrypoint, covering
container restarts after unclean exits.
2026-08-24 13:41:47 +02:00
jklein 818a71f7b6 Clean up stale ip rules on gluetun container start
An uncleanly exited gluetun leaves its ip rule in the shared pod
network namespace; the restarted container then fails forever with
'adding ip rule 101: file exists' while the kill switch keeps blocking
all egress. Apply the upstream-recommended Kubernetes fix: a postStart
hook dropping table-51820 rules.
2026-08-24 13:41:47 +02:00
jklein 2211226c8e Allow writable root filesystem for flaresolverr
The image copies and patches the chromedriver binary to the hardcoded
path /app/chromedriver at startup (no env override exists) and writes
chromedriver caches under /app, so readOnlyRootFilesystem cannot hold;
deviation documented in values.yaml. Reverts the HOME=/tmp workaround,
which only covered the cache path, not the binary patching.
2026-08-24 13:41:46 +02:00
jklein bce2b147a8 Set HOME=/tmp for flaresolverr
The image's home is /app on the read-only root filesystem, but
flaresolverr writes chromedriver caches under $HOME (/app/.local);
pointing HOME at the /tmp emptyDir keeps the root filesystem read-only.
2026-08-24 13:41:46 +02:00
jklein 1ed70e31dd Pin flaresolverr runAsUser/runAsGroup to 1000
The image declares its non-root user by name only, which the kubelet's
runAsNonRoot check cannot verify (CreateContainerConfigError); pinning
the uid/gid numerically satisfies the check.
2026-08-24 13:41:46 +02:00
jklein d1933e0b53 Fix gluetun image tag: Docker Hub tags carry a v prefix
The issue plan listed qmcgaw/gluetun:3.41.1, but the published tag is
v3.41.1. appVersion is the exact image tag per the style guide, so
appVersion becomes v3.41.1 and the chart version 1.0.0+upv3.41.1
(precedent: matrix-synapse, satisfactory, calibre-web-automated).
2026-08-24 13:41:46 +02:00
jklein 914c2d0205 Add freesailarr chart: gluetun-tunneled media automation stack
Single-pod StatefulSet running gluetun (appVersion 3.41.1) as Wireguard
VPN gateway with qbittorrent, prowlarr, flaresolverr, radarr, recyclarr,
sonarr, seerr and bazarr sharing its network namespace. Config volumes
via volumeClaimTemplates, media libraries as existing claims, per-app
enable/ingress toggles, computed FIREWALL_INPUT_PORTS, hardened
security contexts with documented per-image deviations. Wires up
publish workflow, change-app-version choice, install-test exclusion
and README overview.
2026-08-24 13:41:46 +02:00
jklein 6c73ef874b Eight charts: seccompProfile RuntimeDefault and qualified images
Sets seccompProfile.type: RuntimeDefault in the pod security context of
minecraft-router, outline, urlaubsverwaltung, zipline, voucher-vault,
satisfactory, patchmon and calibre-web-automated, so the container
runtime's default syscall filter applies to every container in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Five of the eight also get the docker.io/ prefix on their previously
unqualified image references, so the pull no longer depends on the
runtime's default-registry setting: itzg/mc-router,
outlinewiki/outline, urlaubsverwaltung/urlaubsverwaltung,
l4rm4nd/vouchervault (twice, app and init container) and
guacamole/guacd:1.6.0. zipline, satisfactory and calibre-web-automated
were already qualified with ghcr.io.

Verified in a throwaway k3d cluster against real application work, not
just a Running pod, and with Seccomp: 2 (filter mode) read from
/proc/<pid>/status inside each container:

- minecraft-router: a Minecraft handshake for a mapped domain travels
  through the router to a backend and the backend's status payload comes
  back to the client.
- outline: all migrations apply (45 tables), OIDC discovery succeeds and
  the server-rendered page answers 200 with <title>Outline</title>.
- urlaubsverwaltung: Liquibase applies 79 changesets (35 tables), the app
  seeds its settings and vacation types, /login answers 200.
- zipline: 27 migrations applied, then a full round trip - first user
  created, login, token, upload, and the file downloaded back byte-exact
  from the PVC.
- voucher-vault: the init container's collectstatic output is served
  (200, 22 KB), Django has its 22 tables and uWSGI answers 200 with
  <title>VoucherVault</title>.
- satisfactory: application behavior is NOT proven - the image is
  multiple GB and pulls 10 GB+ of game files on first start, which is why
  the chart sits in ci/excluded-charts.txt. What is proven is that the
  rendered pod securityContext is accepted server-side and yields
  Seccomp: 2 with uid/gid 1000 on a substitute image.
- patchmon: both containers run under the pod profile and both report
  Seccomp: 2. The server applies 47 migrations (44 tables), connects to
  Postgres and Redis and serves its dashboard; guacd answers a real
  Guacamole "select rdp" instruction with its full args list, over the
  same 127.0.0.1:4822 path the chart wires up.
- calibre-web-automated: the readOnlyRootFilesystem: false deviation is
  unaffected - id abc is still uid 1000 (not the 911 the image falls back
  to in read-only mode), /config and the Calibre metadata.db are owned by
  1000, and both the root s6 init and the uid-1000 application report
  Seccomp: 2. calibredb 9.1 runs and adds a book. Its IPC worker prints a
  traceback while doing so, but an unconfined control pod on the same
  image prints the same class of traceback, so it is not the filter.

trivy config now reports "0 to act on" for all eight; the untouched
samba still shows KSV-0104 and KSV-0030, which confirms these charts are
what closed the findings.

No values.schema.json needed changing: in all eight schemas
securityContext.pod is type ["object", "null"] with no property
restrictions, so the new key validates as it stands. The #99 null paths
still hold - nulling securityContext or securityContext.pod falls back to
the defaults including the new one.

Renovate resolves the prefixed guacd reference: a local dry-run lookup
(renovate 44.40.1, --platform=local --dry-run=lookup) reports depName
docker.io/guacamole/guacd against registryUrl https://index.docker.io
with lookupName guacamole/guacd, currentVersion 1.6.0 and its real
release timestamp, with no warnings. The other four prefixed references
live in templates, which Renovate never reads - they are tracked by the
customManagers via appVersion in Chart.yaml with a depName hardcoded in
renovate.json5, so they are unaffected.

Major bump for all eight: runtime behavior changes.

Refs #84
2026-08-24 13:17:14 +02:00
jklein 0d62c0d6ce renovate: name the nginx alpine check in #84 accurately
The rationale called the alpine switch a proposal from the issue text.
A later comment on #84 commissions it as a check with a defined scope
(nginx.conf compatibility, port 8080, uid 101, /tmp redirection) to be
done alongside the seccompProfile PR for this chart. The outcome is open,
the check is not - the constraint has to survive either way.
2026-08-24 12:18:29 +02:00
jklein 2d03ba8a7c renovate: let the nginx constraint survive a variant tag
The anchored form rejected any tag with a suffix. Docker versioning only
offers candidates carrying the current tag's suffix, so the day the
delegation sidecar moves to a slimmer base image (#84, still open) every
candidate would fail the filter and nginx would stop receiving updates -
without an error, a warning, or a red dashboard.

Verified against currentValue 1.28.0-alpine: anchored form returns
"updates": [] and "warnings": [], the suffix-tolerant form proposes
1.30.4-alpine and still refuses the newer 1.31.x mainline.
2026-08-24 12:15:01 +02:00
jklein 0908785b10 renovate: keep the delegation nginx on stable branches
nginx numbers its release branches by minor: even is stable, odd is
mainline. PR #139 proposed 1.30.4 -> 1.31.4, which would have moved the
Synapse delegation sidecar onto mainline without that ever being a
decision. allowedVersions now permits even minors only; patch updates
inside the stable branch still come through and still automerge.
2026-08-24 12:09:26 +02:00
jklein 2e0e37d838 renovate: automerge minor updates, make the install-test blocking
The two open Renovate PRs (#138 alpine 3.22 -> 3.24, #139 nginx 1.30.4 ->
1.31.4) never automerged because both are minor updates while the rule
only covered patch and digest. Sidecar images barely move in patch steps,
so patch-only would practically never fire.

Automerging a chart update publishes it, so the install-test can no
longer report failures without blocking: continue-on-error is gone and
issue #22's stabilization phase ends here. Majors stay manual.
2026-08-24 12:01:08 +02:00
jklein 6b25d082a7 renovate: chart-aware config with an automatic version bump
Renovate only ever rewrites appVersion or an image tag, so every PR it
opened against a chart directory failed the build-chart version check
(#138, #139). ci/renovate-bump-chart.sh recomputes
version: <semver>+up<appVersion> as a postUpgradeTasks hook: chart major
when the app major moves, chart patch otherwise.

Each chart renders its main image as <repo>:{{ .Chart.AppVersion }}, so
repository and version live in different files and Renovate cannot join
them. The mapping is one customManager per chart in renovate.json5,
deliberately not a comment inside each Chart.yaml - that would touch all
20 chart directories and force 20 publishes for a comment change.

Patch and digest updates automerge after a 3-day minimumReleaseAge once
the pipeline is green; majors arrive one line at a time to match the
stepwise app-major procedure. ci/** stays excluded: the install-test
fixtures pin throwaway infrastructure to major lines on purpose.
2026-08-24 11:42:43 +02:00
jklein bcbcc2c62f rss-bridge: app update 2025-06-03 -> 2025-08-05
One step: 2025-08-05 is the immediate successor release, no publication
in between (verified against the project's release list). Tag present on
Docker Hub for amd64 and arm64.

Patch bump, because the changelog review found nothing that touches the
values surface or the chart's behaviour. Checked against the shipped
source of both images rather than against the release notes:

- whitelist.txt format: lib/Configuration.php is byte-identical apart
  from its VERSION constant, so the parsing the chart relies on (trim,
  split on newline, drop empty entries, "*" wildcard) is unchanged.
- the five production bridges: all present, none renamed. Youtube,
  TheHackerNews, ARDMediathek and Reddit are byte-identical; Heise has a
  content-level change only (paywalled heise+ items now carry an
  archive.is link, two more CSS selectors stripped). The three bridges
  removed upstream - Askfm, DansTonChat, FirstLookMediaTech - are none
  of ours.
- RSSBRIDGE_* environment mapping: same file, therefore unchanged.
- write paths: re-derived empirically rather than inferred, because the
  opcache lock file in /tmp appears in no filesystem diff. The new image
  needs exactly the same five, and a control run without /tmp still dies
  with "Unable to create lock file: Read-only file system".
- health endpoint: actions/ is identical, /?action=health intact.
- PHP 8.2.28 -> 8.2.29, nginx and Debian unchanged, so the
  /run/php/php8.2-fpm.sock path the chart mounts still applies. Measured
  fresh-start peak under five concurrent feed fetches: 64.5 MiB on the
  new image against 71.1 MiB on the old one under an identical
  procedure. Requests and limits stay as they are.

The memory comments in values.yaml are refreshed to the image the chart
now ships, and the older 119 MiB figure is labelled as what it was - a
warm container, not a fresh start.

We stay on the dated release tag. The newest release is itself over
twelve months old and the project now pushes latest and commit-based
sha-* tags continuously, but a sha-* tag is not more released than the
release, only newer, and latest is rolling - both conflict with the
repo's convention of reproducible pinned tags.

Fixes #135
2026-08-23 23:59:47 +02:00
jklein 7e6b3beed5 rss-bridge: pin the unprivileged-port sysctl, quote env values
Follow-up to the template-chart hardening fixes in #133, which landed
while this branch was open.

The env block now quotes names, values and keyRef fields. Without it a
value like "5" renders as a YAML number and the API server rejects the
whole manifest. RSS-Bridge takes numeric settings that way
(RSSBRIDGE_http_timeout), so this was reachable with the first env entry
anyone adds.

The port-80 question is settled by measurement rather than by the
"it works in k3d" reasoning #133 warns about. Same pod spec in k3d
(k3s v1.35.5), only ip_unprivileged_port_start changed:

  1024, capabilities.drop [ALL]      nginx: bind() to 0.0.0.0:80 failed
                                     (13: Permission denied)
  1024, plus NET_BIND_SERVICE        identical failure - for a non-root
                                     process the capability lands in the
                                     permitted but not the effective
                                     set, so it buys nothing
  0                                  binds, HTTP 200

So the chart cannot buy safety with a capability the way samba does, and
port 80 is not negotiable either: nginx is configured for it inside the
image and the entrypoint's only lever is a sed against a read-only
/etc/nginx. What it can do is stop depending on the node default - the
pod securityContext now pins the sysctl to 0, so the bind no longer
relies on cluster configuration. Verified: the chart-installed pod
reports 0 and answers 200 through the Service.

Worth recording how that failure looks without probes: the entrypoint
backgrounds nginx and then execs php-fpm, so a failed bind leaves a
container reporting Running with no web server at all. Both test pods
above were Running. The probes on /?action=health are what turn it into
a pod that never becomes Ready.

trivy config still reports 0 to act on; the KSV-0117 statement is
updated to the measured reasoning.
2026-08-23 23:39:49 +02:00
jklein fe6839265c rallly: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Verified in k3d against the database path rather than a static page:
under the filter the app opens its connection to Postgres and Prisma
applies all migrations (141 rows in _prisma_migrations, 31 tables
created), and the server-rendered pages answer with HTTP 200. The kernel
reports Seccomp: 2 (filter mode) inside the container.

Major bump: runtime behavior changes.

Refs #84
2026-08-23 23:37:42 +02:00
jklein e016650f91 rss-bridge: qualify the image registry, except the port-80 finding
Two findings from the Trivy config scan that landed on main in #129.

AVD-KSV-0125 is fixed rather than excepted: writing the image as
docker.io/rssbridge/rss-bridge puts a real registry in the registry
position instead of "rssbridge", which is what ci/trivy/data/ksv0125.yaml
asks for. Verified in k3d that the qualified reference resolves to the
same digest (sha256:0eca99b2...) and still runs.

AVD-KSV-0117 (containerPort below 1024) becomes a scoped, documented
exception. nginx listens on 80 inside the image, and the entrypoint's
only way to change that is a sed against /etc/nginx/sites-enabled/default
which a read-only root filesystem rejects - so the alternative to port 80
is giving up readOnlyRootFilesystem. The premise of the check does not
hold here anyway: the container binds 80 as uid 33 with all capabilities
dropped and no NET_BIND_SERVICE.

trivy config now reports 0 to act on for this chart.
2026-08-23 23:33:23 +02:00
jklein 3fa67d55ef rss-bridge: new chart
Takes over the four loose manifests in cluster-config/rss-bridge/ as a
proper chart, on the current template-chart standard: strict values
schema, null-safe helpers, computed limits, replicas/scaleDown,
checksum/config, NOTES.txt.

Two things deviate from the repo standard on purpose:

- The Ingress models host AND TLS secret name per entry instead of the
  single conventional tls-<release>-<chart>-ingress. The installation
  this replaces has grown three separate certificates, one per host.
  Renaming them would make cert-manager issue three new ones and leave
  each host unreachable until it did - a self-inflicted outage on a
  deployment that has been up for over a year. Entries sharing a secret
  name are grouped into one tls entry, and omitting the name falls back
  to the convention, so consolidating later is a values change.

- Requests and limits are decoupled. The old manifest requested 2Gi of
  ephemeral storage, identical to its limit, reserving two gigabytes for
  a workload measured at 2.2 MB after fetching all five whitelisted
  bridges. The request drops to 64Mi; the limit stays at 2Gi.

Hardening was verified rather than assumed. The image ships no USER and
runs as root by default, but runs fully hardened once its writable paths
are provided: uid/gid 33, readOnlyRootFilesystem, all capabilities
dropped, seccompProfile RuntimeDefault. The writable paths were found by
running the image with a writable rootfs, exercising every bridge and
reading its diff layer - /app/cache, /run, /run/php, /var/lib/nginx.
/tmp did not show up that way, because PHP's opcache creates its lock
file there and unlinks it immediately; it only surfaced when the
hardened container died at startup. Stopping at the first writable path
would have shipped a chart that cannot start.

Probes use the app's own /?action=health, which renders fixed JSON
without touching the cache or any remote site, so neither a scraped site
going down nor an unwritable cache can restart the pod.

Fixes #128
2026-08-23 23:33:23 +02:00
jklein 00ea301e33 template-chart: seccomp profile, unprivileged port, and two placeholder fixes
Five changes to the scaffold every future chart is copied from:

1. seccompProfile.type: RuntimeDefault in the pod security context, in
   line with the rest of the seccomp series (#84). Pod context, because
   from there it also covers init containers and any container added
   later without being repeated.

2. containerPort 80 -> 8080, clearing AVD-KSV-0117. Note that the usual
   justification for this does not hold: a non-root container CAN bind
   port 80 on current Kubernetes, because the kubelet sets
   net.ipv4.ip_unprivileged_port_start=0 in the pod network namespace.
   Measured, not assumed - see the values.yaml comment. The reason to
   stay above 1024 is that this is a runtime default rather than a
   guarantee, and that every real chart here already does.

3. The ConfigMap placeholder key is renamed from the literal "key" to
   "example-setting", clearing AVD-KSV-01010 (secret-scanner patterns
   match any key named "key").

4. The image placeholder is fully qualified as ghcr.io/ORG/IMAGE, so the
   template stops teaching the unqualified form that produced the
   repo's unqualified Docker Hub images.

5. env values are rendered through "quote". Without it a value like
   "8080" renders as a YAML number and the API server rejects the whole
   manifest ("cannot unmarshal number into Go struct field
   EnvVar.value of type string"). Every real chart in the repo already
   quotes here; only the template did not. Found by instantiating the
   template and installing it, not by rendering it.

Verified by instantiating the scaffold into a working chart per the
README procedure and installing it in k3d: the workload becomes Ready,
the Service routes to the app on 8080 and an ephemeral container added
to the running pod inherits Seccomp: 2 from the pod context.

Refs #84
2026-08-23 23:29:51 +02:00
jklein fa8c4fea45 apache-tika: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Verified in k3d against real extractions rather than a reachable port:
the -full image loads its parsers dynamically, so a blocked syscall would
surface as HTTP 200 with empty text, not as an error. The PDF parser, the
HTML parser and the detector/metadata path all return the expected
content under the filter, and the kernel reports Seccomp: 2 (filter mode)
inside the container.

Major bump: runtime behavior changes.

Refs #84
2026-08-23 23:14:54 +02:00
jklein 705ee066a4 CI: record the paperless-ngx root init container as a documented exception
The pod runs rootless as uid/gid 1000; only init-run-ownership is root, to
chown the /run emptyDir that s6-overlay requires to be owned by the runtime
uid. Justified in paperless-ngx/values.yaml, so AVD-KSV-0105/-0022/-0106 are
excepted for that chart with the reason, instead of being reported as if the
workload itself ran as root.
2026-08-23 23:14:12 +02:00
jklein 1ae5e1a7c3 CI: report Trivy misconfiguration findings for changed charts
Adds a trivy-config job to the PR pipeline that renders each changed chart
with Helm and runs `trivy config` over the rendered manifests. Reporting
only - TRIVY_CONFIG_MODE=report always exits 0, so the job never blocks a PR.

Rendered manifests rather than the chart directory: `trivy config <chart-dir>`
renders with default values, which fails on every chart here because of their
required() values - and Trivy does not fail with it, it logs "Skipping chart"
and prints an empty, green report.

Values come from the install-test fixtures (ci/<chart>/test-values.yaml), the
only committed per-chart values in the repo; none of them touches
securityContext, images or capabilities, so the scan sees the chart's own
hardening. Charts without fixtures get ci/trivy/values/<chart>.yaml.

Exceptions live in ci/trivy/trivyignore.yaml in Trivy's native ignore-file
schema, each with a statement - the script refuses to run without one and
reports suppressed findings with their reason instead of hiding them.
AVD-KSV-0125 is configured instead of excepted: ci/trivy/data/ksv0125.yaml
lists the trusted registries, so the check stays able to flag a genuinely
foreign source.

Refs #84.
2026-08-23 23:14:12 +02:00
jklein 37b76e353c gotenberg: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Gotenberg is the chart where a syscall filter was most likely to bite: it
drives headless Chromium and LibreOffice, and browser sandboxes are the
classic casualty of a restrictive seccomp profile. Verified in k3d that
both engines still work under the filter - Chromium HTML->PDF and
LibreOffice document->PDF both return real, non-empty PDFs, and the
kernel reports Seccomp: 2 (filter mode) inside the container.

Major bump: runtime behavior changes.

Refs #84
2026-08-23 23:08:37 +02:00
jklein 4a61f763e8 cyberchef: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod. Verified in k3d: /proc/1/status reports Seccomp: 2 (filter
mode) with one filter loaded, and nginx keeps serving the app.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Major bump: runtime behavior changes, even though it stays invisible for
this workload.

Refs #84
2026-08-23 23:00:02 +02:00
jklein 0c403b6100 gotenberg: small requests, explicit higher limits
The computed limits (limitFactor 3 x request = 1536Mi memory, 30Mi ephemeral
storage) were too small for real documents: a 12 MB HTML already gets the
container OOM-killed, and the caller sees no HTTP error at all, just a dropped
connection and a rising RESTARTS counter.

Rather than raise the requests and reserve conversion-sized capacity on the
node around the clock, requests and limits are decoupled: the requests stay as
they were, and both limits are now set explicitly. The chart already supported
this - an explicit limit wins over the limitFactor computation.

Measured in a throwaway k3d cluster with the official image (HTML via
Chromium), building on the series in the issue:

  18 MB HTML, 2Gi     -> OOMKilled
  18 MB HTML, 2560Mi  -> OOMKilled
  18 MB HTML, 3Gi     -> HTTP 200 in ~22s, 21 MB PDF
  12 MB HTML, 1536Mi  -> OOMKilled (the old computed limit)
  12 MB HTML, 2Gi     -> HTTP 200 in ~13s

Memory beyond 3Gi buys nothing: gotenberg's own request timeout is 30s and the
chart passes no flags to change it, so a document needing more memory has
already run out of time (a 25 MB HTML misses the 30s even at 4Gi).

The ephemeral-storage limit is raised in the same change, because raising
memory alone only moves the failure. With 3Gi memory and the old 30Mi limit,
/tmp peaked at 40 MB on a single conversion and at ~125 MB over a short
series, and the pod was evicted:

  Pod ephemeral local storage usage exceeds the total limit of containers 30Mi

That failure is worse than the OOM-kill: the pod does not restart, it is
replaced, and the old one is left behind as Evicted.

The defaults in the effectiveResources helper are updated in lockstep with
values.yaml, so an erased "resources:" block (#99) falls back to the new
explicit limits instead of the old computed ones.

NOTES.txt is pulled along. Its numbers came from effectiveResources already,
but the text described a threshold that has moved and advice that no longer
holds: raising the request no longer drags the limit with it now that the
limits are explicit. The warning is kept but no longer claims memory is the
first limit you hit - with these limits, the 30s timeout is.

Major, because the runtime behaviour changes and the pod may occupy more
memory during large conversions than it was allowed to before.

Fixes #106
2026-08-23 22:35:22 +02:00
jklein b218f64eac template-chart + README: values.schema.json, gate and null-safe helpers
Closes out the schema programme (#68) by moving what the 18 charts taught
into the reference implementation and the binding guidelines.

template-chart:
- values.schema.json as the reference schema: additionalProperties false
  on every chart-owned level, pod-spec passthrough left open, and the
  reasoning inline in the descriptions so a copy carries it along.
- defaultedDict and subBlock plus the per-block fallbacks for both
  security contexts, the three probes and resources, wired into both the
  Deployment and the StatefulSet.
- the reference itself violated guideline 1: its env block reads
  ingress.domain, so with the Ingress gated off it rendered an empty
  value. That read is now required(), and the comment says why - the
  template is what people copy, so it has to teach the guard, not the
  hole.

README, new section "Values that must not lie" with the five rules:
1. a value read outside its obvious template needs its own guard
   (matrix-synapse published '{"m.server": ":443"}' to federation without
   it, rendering cleanly and reporting healthy)
2. probes, contexts and resources through the null-safe helper, never
   behind an "if" - a guard removes the probe instead of showing a null
3. no liveness on an external dependency where a dependency-free path
   exists, otherwise document why on the helper
4. a fallback reproduces THIS chart's defaults, not the repo standard -
   calibre's readOnlyRootFilesystem: false is what keeps the runtime uid
   at 1000 instead of 911 and existing volume data readable
5. a probe default carries its context - port name, Host header, exec
   command - or the probe vanishes (teamspeak) or gets 403 (patchmon)

Plus the two mechanics that are easiest to lose: null reaches schema
validation only for keys WITHOUT a chart default (which is why the helper
and the ["object","null"] types each cover a case the other cannot), and
schemas must be validated against real bundle values - that step caught a
draft of mine that would have broken three live paperless releases.

CLAUDE.md: the new-chart checklist now names the schema and the
adaptation step, since the copied file is chart-specific and wrong until
it is adapted.

freesailarr is not covered: it is not on main yet (#59).
2026-08-23 22:35:03 +02:00
jklein edb0837246 rallly: default smtp.rejectUnauthorized to true
The deployment renders SMTP_REJECT_UNAUTHORIZED from
rallly.smtp.rejectUnauthorized, but values.yaml never carried the key. With
SMTP enabled the variable therefore reached the app as an empty string --
a certificate check nobody had configured either way.

It now defaults to true, so the mail server's certificate is verified unless
the operator switches the check off deliberately. values.yaml explains when
that is needed (a mail server with a self-signed certificate), because the
symptom otherwise points at the mail server rather than at this value.

The env var is not rendered with "| default true": an explicit false is a
zero value and default would swallow it, silently re-enabling the check the
operator just switched off. A nil falls back to true instead, so the variable
can no longer reach the app empty. The schema now rejects strings.

Operationally inert: the rallly fleet bundle and the deployed release both
have smtp.enabled: false, so the variable is not rendered at all -- the
render with SMTP disabled is byte-identical to before.

Major, because the runtime behaviour of mail sending changes.

Fixes #109
2026-08-23 22:28:24 +02:00
jklein 579433c4bf Add NOTES.txt to the patchmon chart
The centrepiece is the secret inventory: the four OnePasswordItem CRs carry
only item paths, while the fields each item must contain are decided in the
deployment. The notes list them per item — including the eight OIDC fields
nobody would guess — because a missing field does not fail the install, it
starts the container with an empty variable and misbehaves later.

The warnings are the ones this chart earns:

- Sign-in is OIDC-only. The chart sets OIDC_DISABLE_LOCAL_AUTH, so a wrong
  OIDC item locks everyone out with no local account to fall back on.
- A wrong ingress.domain never becomes Ready: the server answers 403 to any
  unknown Host header, the probes send that domain, and the log shows plain
  403s rather than anything resembling a configuration error.
- The first 2.x start rebuilds database indexes and may take minutes, so the
  notes print the actual startup budget and say a slow boot is not a hang.
- The presence registry is keyed by pod hostname, so replicas do not scale.

The startup budget is read through the null-safe probe helper, and the guacd
sidecar and scaled-down states are reported only when they apply. Refs #43.
2026-08-23 22:07:45 +02:00
jklein 1ddb5f457e matrix-synapse: strict schema, ingress gate and null-safe hardening
Three changes, across both workloads (Synapse StatefulSet and delegation
nginx Deployment).

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including the whole matrix.delegation block
   with its own contexts, probes and resources.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   The domain checkpoint found a second reader: the delegation nginx
   serves ingress.domain in its /.well-known/matrix/{server,client}
   endpoints, and read it without a guard. Gating alone would therefore
   have published '{"m.server": ":443"}' and base_url "https://" - a
   federation-breaking answer that renders and starts cleanly. Both reads
   are now required(), so that case fails loudly. Only clusterIssuer
   actually becomes unnecessary when the Ingress is off.

3. Null-safety for hardening, probes and resources (#99) for both
   workloads. The rebuilt probes keep each workload's own endpoint:
   Synapse /health, delegation /.well-known/matrix/server - the endpoint
   that workload exists to serve - including its shorter 3s timeout. The
   delegation pod context keeps having no fsGroup, because it mounts no
   volume that needs one.

The nginx checksum/config annotation from #82 is untouched.

storage stays deliberately not null-safe: an erased block yields an
invalid PVC, which is the loud class #99 excludes.

Rendering is byte-identical to 6.0.0, both with the CI values (which
enable delegation) and with the production bundle values.
2026-08-23 22:05:56 +02:00
jklein 232ecdfa05 samba: remove the unimplemented globalConfig value
samba.globalConfig has been part of values.yaml since the chart's first
commit but is read by no template. Setting it never had an effect.

Rather than implement it, the key is removed and the strict schema now
rejects it: additionalProperties is already false on the samba object, so
dropping the property turns a silently ignored key into a render-time
error.

Operationally inert: neither the cluster-smb fleet bundle nor the deployed
release's user-supplied values set the key -- it only ever appeared as the
chart's own null default.

Major, because a published value disappears and is now rejected.

Fixes #117
2026-08-23 21:59:20 +02:00
jklein ac53555e44 paperless-ngx: strict values.schema.json and null-safe hardening
Largest values surface of the programme, and the first chart where the
null-safety covers two workloads.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including files.barcodes, ocr (with enums
   for the 3.x modes), scripts, tikaGotenberg, the oidc provider block
   and the SMB CronJob.

2. Null-safety for hardening, probes and resources (#99), for the server
   AND the SMB consume-mover CronJob. It reuses the defaultedDict helper
   the barcodes block already introduced (#61) instead of adding a second
   mechanism - one merge helper, now with more callers.

   The server's pod context is consumed twice: as the pod securityContext
   and by the /run-ownership init container, which chowns /run to
   runAsUser:runAsGroup because s6-overlay refuses to start unless /run is
   owned by the runtime uid. Both reads now go through the same helper, so
   the chown cannot disagree with the context it has to match. Erasing
   that block used to abort rendering with a nil-pointer error; it now
   falls back to the defaults like every other chart.

The CronJob has no probes (documented exception), so only its two
contexts and resources are covered.

files.storage stays deliberately not null-safe: an erased value yields an
invalid PVC, which is the loud class #99 excludes.

Rendering is byte-identical to 11.0.0, both with the CI values (which
include the #61 regression guard) and with the production paperless-jk
bundle values.
2026-08-23 21:59:06 +02:00
jklein a76accf7ad minecraft-router: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (clients reach it via a
NodePort), and ingress.domain appears neither in its templates nor in its
values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including the entries of the mappings list,
   where domain and internalUrl are required.

2. Null-safety for hardening, probes and resources (#99). Two things a
   rebuilt default probe must not lose, both verified:
   - the API port is addressed by NAME ("api-port"), so the probe keeps
     working when server.apiPort is overridden.
   - mc-router still has no /health endpoint in 1.46.4, so GET /routes is
     the health signal, and the "Accept: application/json" header belongs
     to that request.

   The pod context keeps pinning uid/gid 10000 even though the image
   declares USER 65532 since 1.46.0, so the runtime identity does not
   silently change with the base image.

mappings stays deliberately not null-safe: the chart fails loudly on a
missing or empty routing table, which is the loud class #99 excludes - a
router without routes is not operable and has no sensible default.

Rendering with normal values is byte-identical to 2.0.0.
2026-08-23 21:49:18 +02:00
jklein 2cb1a05d6f homeassistant: strict values.schema.json and null-safe hardening
No ingress gate change: this chart already honours ingress.enabled, and
its ingress block keeps the upstream-style host/tls lists that the README
documents as an exception. The schema models that shape as it is instead
of pushing the repo's domain/clusterIssuer pair onto it, and the list
entries stay open because they go verbatim into the Ingress spec.

The domain checkpoint ran over values.yaml and templates and came back
empty: ingress.domain does not exist in this chart at all.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Pod-spec passthrough (volumes, volumeMounts,
   nodeSelector, tolerations, affinity, ingress hosts/tls) stays open.

2. Null-safety for hardening, probes and resources (#99).

   This chart's hardening is a documented deviation and the fallbacks
   reproduce it exactly: the pod context is deliberately EMPTY, because
   the official image must start as root (s6-overlay does its setup as
   uid 0 and the image has no non-root user), and capabilities.add keeps
   NET_RAW, without which device discovery (DHCP watcher, ping/arp
   integrations) logs "Operation not permitted".

core.storage stays deliberately not null-safe: an erased block yields an
invalid PVC, which is the loud class #99 excludes.

The #82 limitation around the 1Password-backed config Secret is
untouched: Helm never sees the content, so it cannot be hashed, and the
values.yaml note about the manual rollout restart stays as it is.

Rendering with normal values is byte-identical to 5.0.0.
2026-08-23 21:44:49 +02:00
jklein 24d3366e8b samba: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (SMB and NetBIOS are
exposed via NodePort), and ingress.domain appears neither in its
templates nor in its values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including the entries of the samba.shares
   list.

2. Null-safety for hardening, probes and resources (#99).

   This chart's hardening is a documented deviation forced by the
   ghcr.io/dockur/samba image, and the fallbacks reproduce it exactly
   instead of restoring the repo standard: no runAs* values (the
   entrypoint creates Unix users and smbd impersonates the share user per
   session, both requiring a root start), readOnlyRootFilesystem stays
   false (the entrypoint writes /etc/passwd, /etc/group, the healthcheck
   symlink and the Samba TDBs), and capabilities.add keeps the six
   entries a multi-user root file server needs. Restoring the standard
   here would stop the file server from working.

   The probes target the SMB TCP port directly, because SMB is not HTTP.

The users.conf / checksum-config limitation from #82 is untouched: Helm
never sees the Secret content, so it cannot be hashed, and the comment in
values.yaml documenting the manual rollout restart stays as it is.

Rendering with normal values is byte-identical to 5.0.0.
2026-08-23 21:39:49 +02:00
jklein 6c04a68763 calibre-web-automated: strict schema, ingress gate and null-safe hardening
Three changes, all about values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   The domain checkpoint from rallly ran over values.yaml and templates
   and came back empty here: ingress.domain is read nowhere but in the
   Ingress template. Unlike rallly, zipline, voucher-vault and outline,
   this gate therefore frees both domain and clusterIssuer, and no extra
   required() is needed.

3. Null-safety for hardening, probes and resources (#99).

   This chart's hardening is a documented deviation from the repo
   standard, and the fallbacks reproduce it exactly instead of restoring
   the standard: no runAs* values (s6-overlay boots as root and drops to
   PUID/PGID 1000), readOnlyRootFilesystem stays false (read-only mode
   disables the PUID/PGID remapping, silently moving the runtime uid from
   1000 to 911 and breaking existing volume data), and capabilities.add
   keeps its six entries that the s6 init and supervision need. A
   "cleaned up" standard default here would not be tidier, it would be an
   outage.

cwa.storage stays deliberately not null-safe: an erased value yields an
invalid PVC, which is the loud class #99 excludes.

Rendering with normal values is byte-identical to 4.0.0.
2026-08-23 21:34:32 +02:00
jklein 290c69cc7d satisfactory: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (game and messaging
ports are exposed via NodePort), and ingress.domain appears neither in
its templates nor in its values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Like teamspeak-server, this chart keeps env,
   probes and securityContext at the TOP level, and the schema mirrors
   that shape.

2. Null-safety for hardening, probes and resources (#99). What a rebuilt
   default must not lose here:
   - liveness and startup run the image's own healthcheck script via
     exec. It queries the server's HTTPS API on SERVERGAMEPORT, so it
     survives a port override, which a tcpSocket default could not.
   - readiness keeps the named "api" port.
   - the resources default keeps limitFactor 2 rather than the repo-wide
     3, which is what holds the memory limit at the published 8Gi.
   - the pod context keeps fsGroupChangePolicy OnRootMismatch, which is
     what avoids a recursive chown of multi-GB game files on every start.

server.storage stays deliberately not null-safe: an erased value yields
an invalid PVC, which is the loud class #99 excludes.

This chart is excluded from the CI install-test, so the local proof is
the only one. Rendering is byte-identical to 4.0.0 with default values
and with the production bundle values, and the rendered manifests were
additionally validated by the cluster's API server via a server-side
dry-run.
2026-08-23 21:28:52 +02:00
jklein e54776e6c1 teamspeak-server: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (voice/file-transfer
are exposed via NodePort), and ingress.domain appears neither in its
templates nor in its values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Note that this chart keeps replicas,
   resources, probes and securityContext at the TOP level rather than
   under a chart-named key, so the schema mirrors that shape.

2. Null-safety for hardening, probes and resources (#99). The rebuilt
   probes keep the "file-transfer" port name: the voice service is UDP
   and cannot be probed, so that TCP port is the only thing a probe can
   target - a default probe without it would leave the workload with no
   working probe at all.

   The container defaults keep capabilities.add as an empty list, exactly
   as values.yaml has it, so an erased block restores today's rendering
   byte for byte rather than a cleaned-up variant.

storage stays deliberately not null-safe: an erased block yields an
invalid PVC, which is the loud class #99 excludes.

Rendering with normal values is byte-identical to 4.0.0.
2026-08-23 21:23:30 +02:00
jklein 24ca3513fc outline: strict values.schema.json and null-safe hardening
No ingress gate: this chart already gates its Ingress correctly. Two
changes plus one guard the existing gate turned out to need.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level.

2. Null-safety for hardening, probes and resources (#99). All three
   probes keep /_health when rebuilt: it is Outline's only health
   endpoint, and "/" returns 500 as well when the database is down, so
   there is no dependency-free path to fall back to.

3. ingress.domain is required() where the StatefulSet reads it. It feeds
   the URL env var - Outline's public URL for generated links and the
   OIDC redirect URI - and did so without a guard. With the existing gate
   and no domain it rendered "https://", breaking logins silently. That
   hole predates this change; it now fails loudly.

outline.storage stays deliberately not null-safe: an erased block yields
an invalid PVC, which is the loud class #99 excludes.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 21:18:58 +02:00
jklein f2fc18c6ae voucher-vault: strict values.schema.json and null-safe hardening
No ingress gate: this chart already gates its Ingress correctly. Two
changes plus one guard that the existing gate turned out to need.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Lists that go verbatim into the pod spec
   (ports, storage.volumes, storage.volumeMounts) stay open.

2. Null-safety for hardening, probes and resources (#99). Two specifics
   of this chart:

   - The probes were wrapped in "if" guards, so an erased probe block did
     not render "livenessProbe: null" - it removed the probe entirely and
     silently. The guards are gone; an empty block now means chart
     defaults, so the workload keeps its probes.
   - The rebuilt probes keep the Host: 127.0.0.1 header. Django's
     ALLOWED_HOSTS only accepts 127.0.0.1 and the configured DOMAIN, so a
     probe without it would get 400 and the pod would never be ready.

3. ingress.domain is required() where it is read. The dependency lives in
   values.yaml, not in a template: the DOMAIN env entry is a tpl string
   feeding Django's ALLOWED_HOSTS. With the existing gate and no domain
   the app rendered DOMAIN: "" and answered 400 to every real request -
   silently, and already before this change. It now fails loudly.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 21:14:01 +02:00
jklein 40289a2e67 zipline: strict values.schema.json, ingress gate and null-safe hardening
Three changes, all about values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, so a key the chart does not read is rejected
   at render time instead of being silently ignored.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   As in rallly, ingress.domain is not a pure Ingress value: the
   StatefulSet also feeds it to CORE_DEFAULT_DOMAIN, the app's default
   domain for generated share links, and did so without required(). The
   guarantee only came from the ungated Ingress template, so the gate
   would have handed out unusable links silently. That read is now
   required() as well; only clusterIssuer becomes unnecessary.

3. Null-safety for hardening, probes and resources (#99): an erased block
   fell back to "securityContext: null" (unhardened), a vanished probe or
   a container without resource accounting. The rebuilt liveness probe
   keeps "/" rather than /api/healthcheck, so a database outage still does
   not restart the pod.

config stays deliberately not null-safe: storage and storageClass are
required(), which puts an erased block in the loud class #99 excludes.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 21:07:11 +02:00
jklein 37eda69fdd rallly: strict values.schema.json, ingress gate and null-safe hardening
Three changes, all about values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, so a key the chart does not read is rejected
   at render time instead of being silently ignored.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   ingress.domain is NOT a pure Ingress value here - it is also the app's
   public base URL (NEXT_PUBLIC_BASE_URL). Gating alone would therefore
   have introduced a new silent failure: with the Ingress switched off and
   no domain, the deployment rendered value: "https://" and every
   generated link would be broken. The deployment's read of the value is
   now required(), so that case fails loudly. Only clusterIssuer actually
   becomes unnecessary when the Ingress is off.

3. Null-safety for hardening, probes and resources (#99): an erased block
   fell back to "securityContext: null" (unhardened), a vanished probe or
   a container without resource accounting. Restoring the pod context
   matters especially here, because its fsGroup is what makes the Next.js
   cache emptyDir group-writable for the container.

rallly.cache is deliberately NOT null-safe: cache.path is required(), so
an erased block fails loudly and belongs to the loud class #99 excludes.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 20:58:52 +02:00
jklein f74870b246 apache-tika: note why the notes use effectiveResources
Records the reasoning inline, matching the comment the gotenberg chart
carries, so the next edit does not quietly switch back to the raw helper and
drop the paragraph whenever a resources: block has been erased.
2026-08-23 20:46:50 +02:00
jklein 6295f4b9c1 Add NOTES.txt to the apache-tika chart
Tika has no Ingress, so the notes lead with the cluster-internal URL, the
Service-port-80 to container-port-9998 mismatch a consumer has to get right,
and /parsers as the direct answer to "why did this file come back empty".

Two warnings come from measuring the running container rather than from
reading the values file:

- The image tag has to keep its "-full" suffix. That variant is where
  Tesseract 5.5.0 and the extra parsers live; the plain image carries no
  tesseract binary at all, so dropping the suffix does not fail loudly, it
  just stops OCR and returns empty text for scans.
- The JVM never sees the container limit. It starts without -Xmx and derives
  its heap from the limit at MaxRAMPercentage=25, so the number worth sizing
  against is a quarter of it.

The heap figure is computed from the effective limit through the chart's own
null-safe effectiveResources helper, so an erased resources: block still
prints the defaults the container actually receives instead of dropping the
paragraph. Refs #43.
2026-08-23 20:44:02 +02:00
jklein 8bc673d80d gotenberg: null-safe hardening, probes and resources
Retrofit of #99 for a chart that already carries a values.schema.json,
plus the schema type change that goes with it.

An override written without children is YAML null. Where the chart
defines a default, Helm's coalescing deletes the default before schema
validation runs, so the schema never sees the null and cannot reject it:
the container rendered "securityContext: null" - unhardened, healthy,
silent. Where the chart defines no default (resources.limits), the null
survives into validation and a strict "object" type aborted the render.

The defaultedDict helper from #96 handles the first case; widening the
types of exactly these blocks to ["object", "null"] handles the second.
All other keys stay strict, additionalProperties: false is untouched.

The probes address the container port by name, so a rebuilt default probe
needs no further context.

Rendering with normal values is byte-identical to 5.0.0.
2026-08-23 20:37:12 +02:00
jklein 07ce0bb816 apache-tika: null-safe hardening, probes and resources
Retrofit of #99 for a chart that already carries a values.schema.json,
plus the schema type change that goes with it.

An override written without children is YAML null. Where the chart
defines a default, Helm's coalescing deletes the default before schema
validation runs, so the schema never sees the null and cannot reject it:
the container rendered "securityContext: null" - unhardened, healthy,
silent. Where the chart defines no default (resources.limits), the null
survives into validation and a strict "object" type aborted the render.

The defaultedDict helper from #96 handles the first case; widening the
types of exactly these blocks to ["object", "null"] handles the second.
All other keys stay strict, additionalProperties: false is untouched.

The probes address the container port by name, so a rebuilt default probe
needs no further context.

Rendering with normal values is byte-identical to 8.0.0.
2026-08-23 20:33:00 +02:00
jklein 09d8a94bfc Add NOTES.txt to the gotenberg chart
Gotenberg has no Ingress, so the notes lead with the cluster-internal URL and
point out that the Service listens on 80 while the container listens on 3000
— the mismatch a consumer such as paperless-ngx has to get right — plus the
port-forward command for reaching it by hand.

Three findings from measuring the running chart went in as warnings:

- The chart passes no command-line flags, so Gotenberg's basic auth, OIDC,
  SSRF guards and request timeout cannot be enabled through values at all.
- Memory, not disk, is the first limit to hit: at the default limit a large
  document gets the container OOM-killed, the caller sees the connection
  dropped rather than an HTTP error, and the notes give the exact kubectl
  command that prints OOMKilled.
- Temp space is an emptyDir counted against ephemeral-storage, where running
  out looks different — the pod is evicted and replaced, not restarted.

Both limits are read back through the chart's own resources helper, so the
printed figures follow limitFactor and any override instead of being
hardcoded. Refs #43.
2026-08-23 20:30:22 +02:00
jklein 4e97097a64 cyberchef: honour ingress.enabled, add null-safe hardening
Three changes, all about values that do not do what they promise:

1. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }}. The key already existed in
   values.yaml with default true but was never read, so switching it off
   did nothing. Both required() calls for domain and clusterIssuer live
   inside the Ingress file, so gating also removes the contradiction that
   one had to supply a domain and issuer in order to turn off an Ingress
   one does not want.

2. Null-safety for hardening, probes and resources (#99): an override
   written without children is YAML null and erased the whole default
   sub-tree, rendering "securityContext: null" - an unhardened container
   that starts and reports healthy.

3. values.schema.json types (#68 + #99): the blocks covered by the
   null-safety accept null. This is load-bearing only for keys that have
   no chart default (e.g. resources.limits): there Helm has nothing to
   delete, so the null survives into schema validation and a strict
   "object" type rejected it outright.

Rendering with normal values is byte-identical to 7.0.0.
2026-08-23 20:27:00 +02:00
jklein e6e3ad961e Add NOTES.txt to the cyberchef chart
Prints what an operator would otherwise have to look up after installing:
the rendered Ingress URL, the cert-manager Certificate to watch while the
browser still warns, and the fact that the chart needs no secrets and no
volumes.

Adds three warnings that the values alone do not tell: CyberChef ships no
authentication, so everything that reaches the URL can use it; the app is
static and holds no server-side state, so upgrades and rollbacks need no
migration; and cyberchef.env reaches nginx but never the served bundle.

Scaled-down installations are called out conditionally, naming whichever of
scaleDown / cyberchef.replicas caused it, via the chart's own replicas
helper so the notice cannot drift from the rendered Deployment.

The URL is only ever rendered from .Values.ingress.domain, so no domain is
stored in the chart. Refs #43.
2026-08-23 20:19:41 +02:00
jklein 03c9fd6467 Add null-safe hardening, probes and resources to patchmon
Third change of the same class (#99): an override written without
children is YAML null and replaced the whole default sub-tree, rendering
"securityContext: null" (unhardened container), a vanished probe or a
container without resource accounting - silently, while the workload
reports healthy.

The defaultedDict helper from #96 is adopted for both workloads, so an
empty block means "chart defaults apply" while an explicitly set value
still wins, including false and 0.

The server probes rebuild their Host header from ingress.domain via the
passed-in root context, because the 2.x server rejects requests whose
Host is neither loopback nor a CORS_ORIGIN host.

The schema types allow null for exactly these blocks, so schema and
helper agree instead of the schema rejecting what the helper absorbs.

Rendering with normal values is byte-identical to the previous version,
with and without the guacd sidecar.
2026-08-23 20:19:08 +02:00
jklein 4976c67ce3 Add strict values.schema.json and null-safe hardening to urlaubsverwaltung
Two changes, both about values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, so a key the chart does not read is rejected
   at render time instead of being silently ignored.

2. Null-safety for hardening, probes and resources (#99): an override
   written without children is YAML null and replaced the whole default
   sub-tree, rendering "securityContext: null" (unhardened container), a
   vanished probe or a container without resource accounting - silently.
   The defaultedDict helper from #96 is adopted so an empty block means
   "chart defaults apply", while an explicitly set value still wins,
   including false and 0.

The schema types allow null for exactly these blocks, so the two changes
agree instead of the schema rejecting what the helper is meant to absorb.

No ingress gate here: this chart already gates its Ingress correctly.

Rendering with normal values is byte-identical to the previous version.

Major bump: a schema can break existing deployments that carry dead keys.
2026-08-23 20:14:27 +02:00
jklein a7a6d6da7c Add strict values.schema.json and the ingress gate to patchmon
Two changes of the same class - values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, so a key the chart does not read is rejected
   at render time instead of being silently ignored. Objects passed
   verbatim into the pod spec (probes, securityContext, global) stay open.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged. Without the gate the
   value did not exist at all and could not switch the Ingress off.

Major bump: a schema can break existing deployments that carry dead keys.
2026-08-23 19:34:41 +02:00