Group 1 of the #114 sweep: the eight charts that render exactly one Ingress object and set no behaviour-bearing nginx annotation — bentopdf, garage, patchmon, rallly, searxng, sure, urlaubsverwaltung, voucher-vault. One PR, eight charts, eight minor bumps.
The form is the one PR #115 fixed and template-chart/templates/xxx.httproute.yaml carries; nothing about it was renegotiated. Per chart: an httpRoute block (enabled/parentRefs/hostnames, off by default), a template rendering only an HTTPRoute with parentRefs — no Gateway, no gatewayClassName, no Certificate, no implementation-specific policy object — render aborts on both modes at once (naming both value paths), on an empty parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain], a values.schema.json entry closed against unknown keys with an open parentRef definition, and a NOTES.txt that names the active mode.
The minor proof: eight helm template diffs against origin/main
Rendered with each chart's own ci/<chart>/test-values.yaml (chart defaults alone do not render — the required() secret refs fire first), against the origin/main tree exported with git archive:
Chart
helm template vs origin/main
bentopdf
empty (195 lines)
garage
empty (525 lines)
patchmon
empty (275 lines)
rallly
empty (200 lines)
searxng
empty (260 lines)
sure
two YAML comment lines; every rendered value identical — empty diff over 387 lines / 7 documents once indented # lines are dropped, # Source: headers kept
urlaubsverwaltung
empty (239 lines)
voucher-vault
empty (315 lines)
sure's two lines are the comment its shared env helper emits into the manifest; the explanation moved into a {{/* */}} block that is not rendered, precisely so future edits to the rationale do not show up as diffs in a deployed object. RAILS_ASSUME_SSL/RAILS_FORCE_SSL render "true" before and after.
ingress.clusterIssuer: a no-op in all eight, so nothing was changed
Checked per chart rather than assumed, and the pilot's finding holds eight more times: the onlyrequired() on ingress.clusterIssuer sits inside <chart>.ingress.yaml, already behind {{- if .Values.ingress.enabled }}. NOTES.txt reads it only in the ingress.enabled branch and without required(). So "require it conditionally" would have changed no behaviour in any of the eight, and no template was rebuilt for it. The descriptions were corrected where they claimed something that is now false — that is documentation, not a code path.
ingress.domain is the opposite question and the answer differs per chart:
Chart
second reader of ingress.domain, with its own required()
required in Gateway mode because
bentopdf
none
the HTTPRoute's hostname fallback
garage
none
the HTTPRoute's hostname fallback
patchmon
CORS_ORIGIN (Deployment) — plus the probes' Host header, which uses `
none (UV_MAIL_APPLICATIONURL is a hardcoded placeholder)
the HTTPRoute's hostname fallback
voucher-vault
Django's DOMAIN → ALLOWED_HOSTS
the app, in every mode
Each chart's values comment and schema description now say which of these two it is; three of them (bentopdf, garage, urlaubsverwaltung) previously said "with the Ingress off, domain is free", which stopped being true the moment a second routing template reads it.
Three findings that were not in the plan
1. sure derived its TLS flags from ingress.enabled alone.RAILS_ASSUME_SSL and RAILS_FORCE_SSL were rendered from .Values.ingress.enabled — exactly right while an Ingress was the only way to publish. Gateway mode sets ingress.enabled: falseby construction (the modes are mutually exclusive) while TLS is still terminated, now at the Gateway's listener. Both flags would have come out false: Rails would not trust the forwarded scheme, would hand out session cookies without the Secure attribute over a TLS connection, would send no HSTS and would build absolute URLs — mail links, OIDC redirects — as http://. Nothing fails; the pod is Ready and the UI works.
They now read a new sure.publishedOverTls helper, i.e. ingress.enabled OR httpRoute.enabled. With httpRoute.enabled at its default of false that expression isingress.enabled, which is why the chart stays Minor and the default render is unchanged. Measured in all three modes: Ingress true/true, Gateway true/true, both off false/false.
This is the generalizable one. freesailarr (group 2) and ytdlp-web-player (group 3) also read .Values.ingress.enabled outside their Ingress templates — named in README so the later groups look rather than discover.
2. garage asserted something this PR makes false. Its Ingress template header and its schema both stated, under "guideline 1, checked in both directions", that nothing outside that template reads ingress.domain, so switching the Ingress off freed domain as well as clusterIssuer. garage.httproute.yaml is now a second reader, so the claim holds only while both modes are off. Corrected in both places rather than left standing. garage.s3.rootDomain stays uncoupled — a wildcard vhost suffix and the single host a route answers on are still different things.
3. garage depends on a proxy-body-size it does not set — so the issue's list of five is a list of six.#114 and README name bulwark, calibre-web-automated, outline, paperless-ngx, zipline as the charts that lose their upload limit in Gateway mode, measured on charts that hardcode the annotation. garage sets annotations: {} and therefore passed group 1's screen — but its values comment tells the operator to set nginx.ingress.kubernetes.io/proxy-body-size: "0", because ingress-nginx's 1 MB default truncates S3 uploads. For an object store that is not a detail; it is where PUTs start failing by size. proxy-body-size has no Gateway-API counterpart, so garage's Gateway-mode NOTES.txt prints that the limit is the Gateway's and has to be raised there, and README now names it as a sixth affected chart with the distinction spelled out: "sets no behaviour-bearing annotation" is not the same question as "needs none", and the sweep screens on the second one.
This did not warrant halting the group: the fix fits entirely inside the prescribed form (a NOTES.txt line and a values comment — no new value, no policy object), and the group-1 criterion as written is satisfied by the chart. It is reported here because the criterion is what needs amending for groups 2 and 3, not this chart.
Annotation guidance moved into the Gateway branch
Two of the eight document ingress.annotations as the lever for something the app cannot do itself — the same shape as the pilot's "put auth in front of it" note:
bentopdf — the X-Robots-Tag snippet that keeps a public instance out of search indexes (ROBOTS_NOINDEX cannot run on a read-only root filesystem), and the repair for a controller that overrides the .mjs MIME type or strips the two cross-origin headers. Gateway branch now says both are gone from the chart. The cross-origin headers themselves are sent by the image, so they still arrive in both modes — that note became "two headers have to survive your proxy" with a mode-aware curl, since it is a thing to check rather than configure.
searxng — the Authentik forward-auth block, documented as where a login belongs for an app that has none. Gateway branch says the channel does not exist in the chart at all, and that the rate limiter is unaffected because it runs inside SearXNG against a Valkey, not at the proxy.
The remaining six carry no behaviour-bearing guidance; their annotations comments gained the standard "no counterpart in Gateway mode" paragraph, and patchmon/sure additionally say what does not live there (the WebSocket upgrade works on a plain route, TRUST_PROXY and the TLS flags are environment variables).
garage: the decision and why
garage ships ingress.enabled: false, so by default it publishes nothing in either mode — correct and unchanged. Two consequences:
Its fixture keeps the Ingress on, and the route gets a render overlay.ci/garage/test-values.yaml sets ingress.enabled: true deliberately, so the install-test proves the Service port it references by name (s3) exists. An HTTPRoute cannot make that reference — backendRefs takes a PortNumber — so flipping the fixture to Gateway mode would have traded a check that catches a renamed port for one that does not, and given up garage's only Ingress coverage, for a CRD schema cyberchef's fixture already exercises against the real Gateway-API CRDs. Same reasoning applied to the other seven: install-test stays in Ingress mode, the route is held at render level, which is where its two silent failure modes live.
It is the one chart whose backendRefs port is rendered from a value.garage.ports.s3 is configurable, so a literal 3900 in the route would be a lie the moment anyone changes it. The route renders the port from the same garage.settings helper garage.service.yaml uses, so the two cannot drift by construction — and the assertion resolves it: backendRefs[0] resolves to Service httproute-garage-service:3900. That the test checks Service and port is exactly what makes this provable.
Verification
Run locally, all green:
ci/helm-dependencies.sh per chart; helm lint --strict ×8 at defaults and ×8 with CI values + Gateway overlay — 0 failed in all 16.
helm package ×8 — the eight bumped versions build.
ci/run-httproute-match-test.sh: 10 charts checked, 20 assertions held, 0 failed, 0 not checked — up from 4 assertions over 2 charts (cyberchef, template-chart). Each of the eight contributes "1 parentRef public/gateway" + "backendRefs[0] resolves to <release>-<chart>-service:<port>".
88 render/schema assertions across the eight charts, both directions — 6 rejections each (both modes on, naming both value paths; empty parentRefs; no hostname at all; an unknown key under httpRoute; a parentRef without name; a hostnames entry of the wrong type) and 5 acceptances each (a valid Gateway config; explicit hostnames; an extra ParentReference field upstream may add, proving the definition stays open; httpRoute: erased to YAML null rendering no route, #99; chart defaults rendering no route).
NOTES.txt rendered in all three modes for all eight charts (24 renders, 0 failures) — worth doing explicitly, because CI renders NOTES only through the install-test, whose fixtures stay in Ingress mode, so the Gateway branch is reached by no pipeline stage for these charts.
Not verified, and not claimed
That traffic flows. No Gateway controller runs in CI and none will. Green means a schema-valid object and a Ready pod, not that the route routes.
No runtime check of any kind was run locally: no k3d, no Docker, no Trivy, no cluster. The install-test and trivy config results in this PR are whatever the pipeline produces.
helm install --dry-run could not be used for the NOTES check — this workspace's service account may not read httproutes — so NOTES were rendered offline by copying each chart's NOTES.txt into a throwaway template. That proves the template compiles and every branch evaluates; it says nothing about a cluster.
Scope and what stays open
Untouched, as scoped: rss-bridge (frozen for changes, #51), group 2 (calibre-web-automated, outline, paperless-ngx, zipline, freesailarr), group 3 (homeassistant, keycloak, bulwark, matrix-synapse, stalwart, ytdlp-web-player).
Keyword is Part of #114, not Fixes. Twelve charts remain plus rss-bridge — and #114cannot be closed while #51 is open, because rss-bridge renders an Ingress and is frozen, so the sweep cannot reach completion by working through the groups alone. Whoever lands the last group should expect to leave #114 open and say why.
Wiki: the eight chart pages, Lessons-Learned (the ingress.enabled-derived-value class, which groups 2 and 3 will meet again). No new Charts row and no _Sidebar entry — the charts already exist. Chart-Style-Guide §4 unchanged: the convention was not altered, only applied.
Group 1 of the #114 sweep: the eight charts that render **exactly one** Ingress object and set **no** behaviour-bearing nginx annotation — `bentopdf`, `garage`, `patchmon`, `rallly`, `searxng`, `sure`, `urlaubsverwaltung`, `voucher-vault`. One PR, eight charts, eight **minor** bumps.
The form is the one PR #115 fixed and `template-chart/templates/xxx.httproute.yaml` carries; nothing about it was renegotiated. Per chart: an `httpRoute` block (`enabled`/`parentRefs`/`hostnames`, off by default), a template rendering **only** an `HTTPRoute` with `parentRefs` — no Gateway, no `gatewayClassName`, no Certificate, no implementation-specific policy object — render aborts on both modes at once (naming both value paths), on an empty `parentRefs` and on a missing hostname, empty `hostnames` meaning `[ingress.domain]`, a `values.schema.json` entry closed against unknown keys with an open `parentRef` definition, and a `NOTES.txt` that names the active mode.
## The minor proof: eight `helm template` diffs against `origin/main`
Rendered with each chart's own `ci/<chart>/test-values.yaml` (chart defaults alone do not render — the `required()` secret refs fire first), against the `origin/main` tree exported with `git archive`:
| Chart | `helm template` vs `origin/main` |
| --- | --- |
| `bentopdf` | **empty** (195 lines) |
| `garage` | **empty** (525 lines) |
| `patchmon` | **empty** (275 lines) |
| `rallly` | **empty** (200 lines) |
| `searxng` | **empty** (260 lines) |
| `sure` | **two YAML comment lines**; every rendered *value* identical — empty diff over 387 lines / 7 documents once indented `#` lines are dropped, `# Source:` headers kept |
| `urlaubsverwaltung` | **empty** (239 lines) |
| `voucher-vault` | **empty** (315 lines) |
`sure`'s two lines are the comment its shared env helper emits into the manifest; the explanation moved into a `{{/* */}}` block that is *not* rendered, precisely so future edits to the rationale do not show up as diffs in a deployed object. `RAILS_ASSUME_SSL`/`RAILS_FORCE_SSL` render `"true"` before and after.
## `ingress.clusterIssuer`: a no-op in all eight, so nothing was changed
Checked per chart rather than assumed, and the pilot's finding holds eight more times: the **only** `required()` on `ingress.clusterIssuer` sits inside `<chart>.ingress.yaml`, already behind `{{- if .Values.ingress.enabled }}`. `NOTES.txt` reads it only in the `ingress.enabled` branch and without `required()`. So "require it conditionally" would have changed no behaviour in any of the eight, and no template was rebuilt for it. The *descriptions* were corrected where they claimed something that is now false — that is documentation, not a code path.
`ingress.domain` is the opposite question and the answer differs per chart:
| Chart | second reader of `ingress.domain`, with its own `required()` | required in Gateway mode because |
| --- | --- | --- |
| `bentopdf` | none | the HTTPRoute's hostname fallback |
| `garage` | none | the HTTPRoute's hostname fallback |
| `patchmon` | `CORS_ORIGIN` (Deployment) — plus the probes' `Host` header, which uses `| default ""` and demands nothing | the app, in every mode |
| `rallly` | `NEXT_PUBLIC_BASE_URL` | the app, in every mode |
| `searxng` | `SEARXNG_BASE_URL` | the app, in every mode |
| `sure` | `APP_DOMAIN`, `WEBAUTHN_RP_ID`, `WEBAUTHN_ALLOWED_ORIGINS` | the app, in every mode |
| `urlaubsverwaltung` | none (`UV_MAIL_APPLICATIONURL` is a hardcoded placeholder) | the HTTPRoute's hostname fallback |
| `voucher-vault` | Django's `DOMAIN` → `ALLOWED_HOSTS` | the app, in every mode |
Each chart's values comment and schema description now say which of these two it is; three of them (`bentopdf`, `garage`, `urlaubsverwaltung`) previously said "with the Ingress off, `domain` is free", which stopped being true the moment a second routing template reads it.
## Three findings that were not in the plan
**1. `sure` derived its TLS flags from `ingress.enabled` alone.** `RAILS_ASSUME_SSL` and `RAILS_FORCE_SSL` were rendered from `.Values.ingress.enabled` — exactly right while an Ingress was the only way to publish. Gateway mode sets `ingress.enabled: false` **by construction** (the modes are mutually exclusive) while TLS is still terminated, now at the Gateway's listener. Both flags would have come out `false`: Rails would not trust the forwarded scheme, would hand out session cookies **without the Secure attribute over a TLS connection**, would send no HSTS and would build absolute URLs — mail links, OIDC redirects — as `http://`. Nothing fails; the pod is Ready and the UI works.
They now read a new `sure.publishedOverTls` helper, i.e. `ingress.enabled OR httpRoute.enabled`. With `httpRoute.enabled` at its default of `false` that expression **is** `ingress.enabled`, which is why the chart stays Minor and the default render is unchanged. Measured in all three modes: Ingress `true/true`, Gateway `true/true`, both off `false/false`.
This is the generalizable one. `freesailarr` (group 2) and `ytdlp-web-player` (group 3) also read `.Values.ingress.enabled` outside their Ingress templates — named in README so the later groups look rather than discover.
**2. `garage` asserted something this PR makes false.** Its Ingress template header and its schema both stated, under "guideline 1, checked in both directions", that *nothing* outside that template reads `ingress.domain`, so switching the Ingress off freed `domain` as well as `clusterIssuer`. `garage.httproute.yaml` is now a second reader, so the claim holds only while **both** modes are off. Corrected in both places rather than left standing. `garage.s3.rootDomain` stays uncoupled — a wildcard vhost suffix and the single host a route answers on are still different things.
**3. `garage` depends on a `proxy-body-size` it does not set — so the issue's list of five is a list of six.** #114 and README name `bulwark`, `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline` as the charts that lose their upload limit in Gateway mode, measured on charts that *hardcode* the annotation. `garage` sets `annotations: {}` and therefore passed group 1's screen — but its values comment tells the operator to set `nginx.ingress.kubernetes.io/proxy-body-size: "0"`, because ingress-nginx's 1 MB default truncates S3 uploads. For an object store that is not a detail; it is where PUTs start failing by size. `proxy-body-size` has no Gateway-API counterpart, so `garage`'s Gateway-mode `NOTES.txt` prints that the limit is the Gateway's and has to be raised there, and README now names it as a sixth affected chart with the distinction spelled out: "sets no behaviour-bearing annotation" is not the same question as "needs none", and the sweep screens on the second one.
This did not warrant halting the group: the fix fits entirely inside the prescribed form (a `NOTES.txt` line and a values comment — no new value, no policy object), and the group-1 criterion as written is satisfied by the chart. It is reported here because the *criterion* is what needs amending for groups 2 and 3, not this chart.
## Annotation guidance moved into the Gateway branch
Two of the eight document `ingress.annotations` as the lever for something the app cannot do itself — the same shape as the pilot's "put auth in front of it" note:
- **`bentopdf`** — the `X-Robots-Tag` snippet that keeps a public instance out of search indexes (`ROBOTS_NOINDEX` cannot run on a read-only root filesystem), and the repair for a controller that overrides the `.mjs` MIME type or strips the two cross-origin headers. Gateway branch now says both are gone from the chart. The cross-origin headers themselves are sent by the *image*, so they still arrive in both modes — that note became "two headers have to survive your **proxy**" with a mode-aware `curl`, since it is a thing to check rather than configure.
- **`searxng`** — the Authentik forward-auth block, documented as where a login belongs for an app that has none. Gateway branch says the channel does not exist in the chart at all, and that the rate limiter is unaffected because it runs inside SearXNG against a Valkey, not at the proxy.
The remaining six carry no behaviour-bearing guidance; their `annotations` comments gained the standard "no counterpart in Gateway mode" paragraph, and `patchmon`/`sure` additionally say what does *not* live there (the WebSocket upgrade works on a plain route, `TRUST_PROXY` and the TLS flags are environment variables).
## `garage`: the decision and why
`garage` ships `ingress.enabled: false`, so by default it publishes nothing in **either** mode — correct and unchanged. Two consequences:
- **Its fixture keeps the Ingress on, and the route gets a render overlay.** `ci/garage/test-values.yaml` sets `ingress.enabled: true` deliberately, so the install-test proves the Service port it references **by name** (`s3`) exists. An HTTPRoute cannot make that reference — `backendRefs` takes a `PortNumber` — so flipping the fixture to Gateway mode would have traded a check that catches a renamed port for one that does not, *and* given up garage's only Ingress coverage, for a CRD schema `cyberchef`'s fixture already exercises against the real Gateway-API CRDs. Same reasoning applied to the other seven: install-test stays in Ingress mode, the route is held at render level, which is where its two silent failure modes live.
- **It is the one chart whose `backendRefs` port is rendered from a value.** `garage.ports.s3` is configurable, so a literal `3900` in the route would be a lie the moment anyone changes it. The route renders the port from the same `garage.settings` helper `garage.service.yaml` uses, so the two cannot drift by construction — and the assertion resolves it: `backendRefs[0] resolves to Service httproute-garage-service:3900`. That the test checks Service **and port** is exactly what makes this provable.
## Verification
Run locally, all green:
- `ci/helm-dependencies.sh` per chart; `helm lint --strict` ×8 at defaults **and** ×8 with CI values + Gateway overlay — 0 failed in all 16.
- `helm package` ×8 — the eight bumped versions build.
- `ci/run-httproute-match-test.sh`: **10 charts checked, 20 assertions held, 0 failed, 0 not checked** — up from 4 assertions over 2 charts (`cyberchef`, `template-chart`). Each of the eight contributes "1 parentRef `public/gateway`" + "`backendRefs[0]` resolves to `<release>-<chart>-service:<port>`".
- `ci/run-quantity-render-test.sh`: 28 exercised, 0 failed. `ci/run-selector-match-test.sh`: 0 failed.
- **88 render/schema assertions** across the eight charts, both directions — 6 rejections each (both modes on, naming both value paths; empty `parentRefs`; no hostname at all; an unknown key under `httpRoute`; a `parentRef` without `name`; a `hostnames` entry of the wrong type) and 5 acceptances each (a valid Gateway config; explicit `hostnames`; an extra `ParentReference` field upstream may add, proving the definition stays open; `httpRoute:` erased to YAML null rendering no route, #99; chart defaults rendering no route).
- `NOTES.txt` rendered in **all three modes for all eight charts** (24 renders, 0 failures) — worth doing explicitly, because CI renders NOTES only through the install-test, whose fixtures stay in Ingress mode, so the Gateway branch is reached by no pipeline stage for these charts.
### Not verified, and not claimed
- **That traffic flows.** No Gateway controller runs in CI and none will. Green means a schema-valid object and a `Ready` pod, not that the route routes.
- **No runtime check of any kind was run locally**: no k3d, no Docker, no Trivy, no cluster. The install-test and `trivy config` results in this PR are whatever the pipeline produces.
- `helm install --dry-run` could not be used for the NOTES check — this workspace's service account may not read `httproutes` — so NOTES were rendered offline by copying each chart's `NOTES.txt` into a throwaway template. That proves the template compiles and every branch evaluates; it says nothing about a cluster.
## Scope and what stays open
Untouched, as scoped: `rss-bridge` (frozen for changes, #51), group 2 (`calibre-web-automated`, `outline`, `paperless-ngx`, `zipline`, `freesailarr`), group 3 (`homeassistant`, `keycloak`, `bulwark`, `matrix-synapse`, `stalwart`, `ytdlp-web-player`).
**Keyword is `Part of #114`, not `Fixes`.** Twelve charts remain plus `rss-bridge` — and #114 **cannot be closed while #51 is open**, because `rss-bridge` renders an Ingress and is frozen, so the sweep cannot reach completion by working through the groups alone. Whoever lands the last group should expect to leave #114 open and say why.
Wiki: the eight chart pages, `Lessons-Learned` (the `ingress.enabled`-derived-value class, which groups 2 and 3 will meet again). No new `Charts` row and no `_Sidebar` entry — the charts already exist. `Chart-Style-Guide` §4 unchanged: the convention was not altered, only applied.
Part of #114
Group 1 of the #114 sweep: the charts that render exactly one Ingress object
and set no behaviour-bearing nginx annotation - bentopdf, garage, patchmon,
rallly, searxng, sure, urlaubsverwaltung, voucher-vault.
Each gets the form the pilot (cyberchef, PR #115) fixed and template-chart
carries: an httpRoute block (enabled/parentRefs/hostnames, off by default), a
template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no
gatewayClassName, no Certificate, no implementation-specific policy object -
render aborts on both modes at once (naming both value paths), on an empty
parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain],
a schema entry closed against unknown keys with an open parentRef definition,
and a NOTES.txt that names the active mode and what the mode costs.
Minor, additive, defaults unchanged: `helm template` with each chart's CI
values is byte-identical to origin/main for seven of the eight, and for sure
identical in every rendered value (the diff is two YAML comment lines).
Three findings that were not in the plan, each argued in the file it changes:
sure derived RAILS_ASSUME_SSL/RAILS_FORCE_SSL from ingress.enabled alone.
Gateway mode sets that false by construction while TLS is still terminated at
the Gateway, so both flags would have gone false: insecure cookies, no HSTS,
http:// links, with a Ready pod and a working UI as the only symptom. They now
read sure.publishedOverTls (ingress.enabled OR httpRoute.enabled), which is
ingress.enabled while httpRoute.enabled is false - hence still Minor.
garage claimed, in its Ingress template and its schema, that nothing outside
that template reads ingress.domain. The HTTPRoute reads it, so the claim now
holds only while both modes are off. Corrected rather than left standing.
garage also depends on an upload limit it does not set: its annotations
comment tells the operator to set proxy-body-size: "0" because ingress-nginx
truncates S3 uploads at 1 MB. proxy-body-size has no Gateway-API counterpart,
so its Gateway-mode NOTES prints that the limit is the Gateway's. README's
list of affected charts named five and now names garage as a sixth.
clusterIssuer needed no change in any of the eight: its only required() sits
inside the Ingress template behind ingress.enabled, the same no-op the pilot
found. Annotation guidance that stops applying in Gateway mode moved into the
Gateway branch of NOTES.txt for bentopdf (X-Robots-Tag) and searxng
(forward-auth).
CI: one render overlay per chart under ci/httproute/, so the assertion is not
vacuum-green - ci/run-httproute-match-test.sh goes from 4 held assertions over
2 charts to 20 over 10. The install-test fixtures stay in Ingress mode: the
install runs one configuration per chart, cyberchef already pays that price
against the real Gateway-API CRDs, and for garage the Ingress is the only place
its Service port is referenced by NAME.
Not proven, by construction: that traffic flows. No Gateway controller runs in
CI. Not run locally either: k3d, Docker, Trivy.
Part of #114
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Group 1 of the #114 sweep: the eight charts that render exactly one Ingress object and set no behaviour-bearing nginx annotation —
bentopdf,garage,patchmon,rallly,searxng,sure,urlaubsverwaltung,voucher-vault. One PR, eight charts, eight minor bumps.The form is the one PR #115 fixed and
template-chart/templates/xxx.httproute.yamlcarries; nothing about it was renegotiated. Per chart: anhttpRouteblock (enabled/parentRefs/hostnames, off by default), a template rendering only anHTTPRoutewithparentRefs— no Gateway, nogatewayClassName, no Certificate, no implementation-specific policy object — render aborts on both modes at once (naming both value paths), on an emptyparentRefsand on a missing hostname, emptyhostnamesmeaning[ingress.domain], avalues.schema.jsonentry closed against unknown keys with an openparentRefdefinition, and aNOTES.txtthat names the active mode.The minor proof: eight
helm templatediffs againstorigin/mainRendered with each chart's own
ci/<chart>/test-values.yaml(chart defaults alone do not render — therequired()secret refs fire first), against theorigin/maintree exported withgit archive:helm templatevsorigin/mainbentopdfgaragepatchmonralllysearxngsure#lines are dropped,# Source:headers kepturlaubsverwaltungvoucher-vaultsure's two lines are the comment its shared env helper emits into the manifest; the explanation moved into a{{/* */}}block that is not rendered, precisely so future edits to the rationale do not show up as diffs in a deployed object.RAILS_ASSUME_SSL/RAILS_FORCE_SSLrender"true"before and after.ingress.clusterIssuer: a no-op in all eight, so nothing was changedChecked per chart rather than assumed, and the pilot's finding holds eight more times: the only
required()oningress.clusterIssuersits inside<chart>.ingress.yaml, already behind{{- if .Values.ingress.enabled }}.NOTES.txtreads it only in theingress.enabledbranch and withoutrequired(). So "require it conditionally" would have changed no behaviour in any of the eight, and no template was rebuilt for it. The descriptions were corrected where they claimed something that is now false — that is documentation, not a code path.ingress.domainis the opposite question and the answer differs per chart:ingress.domain, with its ownrequired()bentopdfgaragepatchmonCORS_ORIGIN(Deployment) — plus the probes'Hostheader, which uses `ralllyNEXT_PUBLIC_BASE_URLsearxngSEARXNG_BASE_URLsureAPP_DOMAIN,WEBAUTHN_RP_ID,WEBAUTHN_ALLOWED_ORIGINSurlaubsverwaltungUV_MAIL_APPLICATIONURLis a hardcoded placeholder)voucher-vaultDOMAIN→ALLOWED_HOSTSEach chart's values comment and schema description now say which of these two it is; three of them (
bentopdf,garage,urlaubsverwaltung) previously said "with the Ingress off,domainis free", which stopped being true the moment a second routing template reads it.Three findings that were not in the plan
1.
surederived its TLS flags fromingress.enabledalone.RAILS_ASSUME_SSLandRAILS_FORCE_SSLwere rendered from.Values.ingress.enabled— exactly right while an Ingress was the only way to publish. Gateway mode setsingress.enabled: falseby construction (the modes are mutually exclusive) while TLS is still terminated, now at the Gateway's listener. Both flags would have come outfalse: Rails would not trust the forwarded scheme, would hand out session cookies without the Secure attribute over a TLS connection, would send no HSTS and would build absolute URLs — mail links, OIDC redirects — ashttp://. Nothing fails; the pod is Ready and the UI works.They now read a new
sure.publishedOverTlshelper, i.e.ingress.enabled OR httpRoute.enabled. WithhttpRoute.enabledat its default offalsethat expression isingress.enabled, which is why the chart stays Minor and the default render is unchanged. Measured in all three modes: Ingresstrue/true, Gatewaytrue/true, both offfalse/false.This is the generalizable one.
freesailarr(group 2) andytdlp-web-player(group 3) also read.Values.ingress.enabledoutside their Ingress templates — named in README so the later groups look rather than discover.2.
garageasserted something this PR makes false. Its Ingress template header and its schema both stated, under "guideline 1, checked in both directions", that nothing outside that template readsingress.domain, so switching the Ingress off freeddomainas well asclusterIssuer.garage.httproute.yamlis now a second reader, so the claim holds only while both modes are off. Corrected in both places rather than left standing.garage.s3.rootDomainstays uncoupled — a wildcard vhost suffix and the single host a route answers on are still different things.3.
garagedepends on aproxy-body-sizeit does not set — so the issue's list of five is a list of six. #114 and README namebulwark,calibre-web-automated,outline,paperless-ngx,ziplineas the charts that lose their upload limit in Gateway mode, measured on charts that hardcode the annotation.garagesetsannotations: {}and therefore passed group 1's screen — but its values comment tells the operator to setnginx.ingress.kubernetes.io/proxy-body-size: "0", because ingress-nginx's 1 MB default truncates S3 uploads. For an object store that is not a detail; it is where PUTs start failing by size.proxy-body-sizehas no Gateway-API counterpart, sogarage's Gateway-modeNOTES.txtprints that the limit is the Gateway's and has to be raised there, and README now names it as a sixth affected chart with the distinction spelled out: "sets no behaviour-bearing annotation" is not the same question as "needs none", and the sweep screens on the second one.This did not warrant halting the group: the fix fits entirely inside the prescribed form (a
NOTES.txtline and a values comment — no new value, no policy object), and the group-1 criterion as written is satisfied by the chart. It is reported here because the criterion is what needs amending for groups 2 and 3, not this chart.Annotation guidance moved into the Gateway branch
Two of the eight document
ingress.annotationsas the lever for something the app cannot do itself — the same shape as the pilot's "put auth in front of it" note:bentopdf— theX-Robots-Tagsnippet that keeps a public instance out of search indexes (ROBOTS_NOINDEXcannot run on a read-only root filesystem), and the repair for a controller that overrides the.mjsMIME type or strips the two cross-origin headers. Gateway branch now says both are gone from the chart. The cross-origin headers themselves are sent by the image, so they still arrive in both modes — that note became "two headers have to survive your proxy" with a mode-awarecurl, since it is a thing to check rather than configure.searxng— the Authentik forward-auth block, documented as where a login belongs for an app that has none. Gateway branch says the channel does not exist in the chart at all, and that the rate limiter is unaffected because it runs inside SearXNG against a Valkey, not at the proxy.The remaining six carry no behaviour-bearing guidance; their
annotationscomments gained the standard "no counterpart in Gateway mode" paragraph, andpatchmon/sureadditionally say what does not live there (the WebSocket upgrade works on a plain route,TRUST_PROXYand the TLS flags are environment variables).garage: the decision and whygarageshipsingress.enabled: false, so by default it publishes nothing in either mode — correct and unchanged. Two consequences:ci/garage/test-values.yamlsetsingress.enabled: truedeliberately, so the install-test proves the Service port it references by name (s3) exists. An HTTPRoute cannot make that reference —backendRefstakes aPortNumber— so flipping the fixture to Gateway mode would have traded a check that catches a renamed port for one that does not, and given up garage's only Ingress coverage, for a CRD schemacyberchef's fixture already exercises against the real Gateway-API CRDs. Same reasoning applied to the other seven: install-test stays in Ingress mode, the route is held at render level, which is where its two silent failure modes live.backendRefsport is rendered from a value.garage.ports.s3is configurable, so a literal3900in the route would be a lie the moment anyone changes it. The route renders the port from the samegarage.settingshelpergarage.service.yamluses, so the two cannot drift by construction — and the assertion resolves it:backendRefs[0] resolves to Service httproute-garage-service:3900. That the test checks Service and port is exactly what makes this provable.Verification
Run locally, all green:
ci/helm-dependencies.shper chart;helm lint --strict×8 at defaults and ×8 with CI values + Gateway overlay — 0 failed in all 16.helm package×8 — the eight bumped versions build.ci/run-httproute-match-test.sh: 10 charts checked, 20 assertions held, 0 failed, 0 not checked — up from 4 assertions over 2 charts (cyberchef,template-chart). Each of the eight contributes "1 parentRefpublic/gateway" + "backendRefs[0]resolves to<release>-<chart>-service:<port>".ci/run-quantity-render-test.sh: 28 exercised, 0 failed.ci/run-selector-match-test.sh: 0 failed.parentRefs; no hostname at all; an unknown key underhttpRoute; aparentRefwithoutname; ahostnamesentry of the wrong type) and 5 acceptances each (a valid Gateway config; explicithostnames; an extraParentReferencefield upstream may add, proving the definition stays open;httpRoute:erased to YAML null rendering no route, #99; chart defaults rendering no route).NOTES.txtrendered in all three modes for all eight charts (24 renders, 0 failures) — worth doing explicitly, because CI renders NOTES only through the install-test, whose fixtures stay in Ingress mode, so the Gateway branch is reached by no pipeline stage for these charts.Not verified, and not claimed
Readypod, not that the route routes.trivy configresults in this PR are whatever the pipeline produces.helm install --dry-runcould not be used for the NOTES check — this workspace's service account may not readhttproutes— so NOTES were rendered offline by copying each chart'sNOTES.txtinto a throwaway template. That proves the template compiles and every branch evaluates; it says nothing about a cluster.Scope and what stays open
Untouched, as scoped:
rss-bridge(frozen for changes, #51), group 2 (calibre-web-automated,outline,paperless-ngx,zipline,freesailarr), group 3 (homeassistant,keycloak,bulwark,matrix-synapse,stalwart,ytdlp-web-player).Keyword is
Part of #114, notFixes. Twelve charts remain plusrss-bridge— and #114 cannot be closed while #51 is open, becauserss-bridgerenders an Ingress and is frozen, so the sweep cannot reach completion by working through the groups alone. Whoever lands the last group should expect to leave #114 open and say why.Wiki: the eight chart pages,
Lessons-Learned(theingress.enabled-derived-value class, which groups 2 and 3 will meet again). No newChartsrow and no_Sidebarentry — the charts already exist.Chart-Style-Guide§4 unchanged: the convention was not altered, only applied.Part of #114