7 Commits
Author SHA1 Message Date
jklein bf7640104f docs(README): the MTA-STS delta measured on top of the landed rss-bridge round
Validate Helm Charts / Detect changed charts (pull_request) Successful in 15s
Validate Helm Charts / Resource quantities (pull_request) Successful in 18s
Validate Helm Charts / Selector match (pull_request) Successful in 18s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Build stalwart (pull_request) Successful in 10s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 23s
Install-test / Install-test stalwart (pull_request) Successful in 1m36s
Validate Helm Charts / Install-test (pull_request) Successful in 1m36s
The rss-bridge round of #114 merged while this PR was open, and it rewrote
the HTTPRoute-test paragraph with a convention this round now follows: the
three parallel rounds measured on branches that did not contain each other,
so the figure to trust from any one of them is its DELTA, not its total.

Rebased onto it, kept its paragraph word for word, and appended this
round's delta rather than replacing the sentence - the first entry in that
history where the figure moves because a chart ALREADY in the list gained
an object instead of because a chart joined.

Re-measured after the rebase rather than carried over: 60 assertions over
22 charts, read from ci/run-httproute-match-test.sh on this branch. The
earlier 58/21 in this PR's description was correct for a branch without
rss-bridge and is not a running total any more. #122's four assertions are
still outstanding, so whoever merges that one has the same sentence to
extend.
2026-10-13 10:32:13 +00:00
jklein 87e138254e fix(stalwart): let the mode conflict, not the MTA-STS entry, name the defect
With BOTH routing modes on and an mtaSts entry missing its tlsSecretName,
the render reported

  mtaSts.hosts[0] (mta-sts.example.com) has no tlsSecretName, and in
  Ingress mode every entry needs one

which is true and is the wrong diagnosis: the values file's defect is that
ingress.enabled and httpRoute.enabled are both true, and "in Ingress mode"
is a claim about a configuration that is not coherent at all. Whoever met
it would add a tlsSecretName, re-run, and reach the real message one round
later - or conclude the MTA-STS feature was the problem.

Measured on this chart, not reasoned about. And it is the second occurrence
of a defect the parallel keycloak round of Gitea #122 found on its own
realm list, independently and in the same round, which is what makes it a
rule rather than one chart's story: a specific validation must not be
reachable in a state that a structural validation already rejects.

Ordering is not the fix and could not be - Helm renders templates in an
order a chart cannot pin, so this template may not assume the httproute
template's abort runs first. Scope is: the Ingress-mode MTA-STS block is
now gated on ingress.enabled AND NOT httpRoute.enabled, so in the
incoherent combination it validates nothing and the only fail left is the
one that names the real problem.

The per-entry guard is NOT dead by this - verified reachable with
ingress.enabled alone. Defaults, the unchanged CI fixture and the overlay
all still render byte-identically against origin/main, and the HTTPRoute
render test still holds 58 assertions over 21 charts.
2026-10-13 10:31:36 +00:00
jklein ee477288fc feat(stalwart): MTA-STS routes as a maintainable list, in both routing modes
The policy route for mta-sts.<domain> belonged to another repository
(jk-cluster-config) while pointing at this chart's own Service. It moves
here as mtaSts.hosts, a list, so several domains - subdomains included -
are maintainable in one place and in whichever routing mode is active.

THE LIST IS THE SWITCH. No mtaSts.enabled: "enabled with an empty list"
would be a state with no meaning that the chart would then have to catch.
The schema rejects the key outright, so the rule is enforced rather than
documented. Default [] renders nothing in either mode - the defaults and
the unchanged CI fixture both produce a byte-identical helm template
against origin/main.

Entry form {host, tlsSecretName, clusterIssuer?}. The two TLS fields are
read ONLY in Ingress mode; in Gateway mode TLS sits on the listener, so
requiring them there would demand a value that does nothing.

THE PATH IS HARDWIRED. RFC 8461 fixes /.well-known/mta-sts.txt, rendered
with pathType Exact (type: Exact in the route). A values key for a
protocol-mandated path can only ever be set wrong, and wrong is invisible
from this side. Exact also matters in itself: a prefix on these hostnames
would publish the mail server's whole management surface under them.

ONE INGRESS PER CLUSTER ISSUER, NOT ONE OBJECT - and the plan's own three
requirements forced that. cert-manager reads the two fields at different
scopes: cluster-issuer is an ANNOTATION (object-scoped, and absent means
nothing is issued for that object at all), while secretName is
tls-entry-scoped and yields one Certificate per entry. So an object cannot
both request a certificate and leave another host's secret alone, which is
what an empty per-entry issuer has to mean. Measured in jk-cluster:
ingress-shim names the Certificate AFTER THE SECRET and owns it from the
Ingress, so a shared secret must sit in exactly one object (hence grouping
the tls list by secret, reusing rss-bridge.tlsGroups' shape) and a secret
may not span two issuer groups (hence the abort). Objects are named by
issuer, not by index: an index is only free where the object holds no
certificate, which is the precondition homeassistant's route naming states
for itself.

Gateway mode renders ONE HTTPRoute with every hostname - nothing differs
per host there - attached to the same Gateway(s) as the WebUI route.

WHAT THE CHART CANNOT PROMISE, AND THEREFORE NAMES: it registers a route,
it does not create a domain. The domains live in Stalwart's data store, so
an entry whose domain is missing there answers 404 at a correctly routed
endpoint with every object green - and MTA-STS failures are silent on the
SENDER's side, which delivers in the clear and logs nothing here. Same for
the _mta-sts.<domain> TXT record and the policy's content. The subdomain
trap is documented too: mta-sts.sub.<domain> is one label below
*.<domain>, so a parent wildcard certificate does not cover it.

Not folded into certificate.extraDnsNames deliberately - that would hang
the MAIL certificate on this list, letting a bad policy entry take STARTTLS
and IMAPS with it.

Minor, additive, empty default: 1.2.0+upv0.16.25 -> 1.3.0+upv0.16.25.

CI: ci/httproute/stalwart.yaml enters all three shapes of the entry form
(with issuer, without, and a subdomain), taking the HTTPRoute render test
from 56 assertions over 21 charts to 58 over 21 - stalwart from 2 to 4.
The Ingress half cannot be covered there, since an overlay's whole job is
to switch the mode off, so ci/stalwart/test-values.yaml carries two
entries and the install-test has a real API server accept them.

Fixes #123
2026-10-13 10:31:36 +00:00
jklein 8896fc8d7d docs(README): the 58/22 figure is a delta, not a running total
Validate Helm Charts / Detect changed charts (pull_request) Successful in 7s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 18s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 18s
Validate Helm Charts / Selector match (pull_request) Successful in 15s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 16s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m4s
Validate Helm Charts / Install-test (pull_request) Successful in 1m4s
SonarQube Analysis / SonarQube Analysis (push) Successful in 45s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 12s
Publish RSS-Bridge Helm Chart / Publish RSS-Bridge Helm Chart (push) Successful in 12s
Gitea #122 (one hostname per keycloak realm) is in flight concurrently and
raises keycloak's share from 4 assertions to 8. Its round was measured at
60 over 21 on a branch that does not contain rss-bridge, while the 58 over
22 here was read on a branch that does not contain the realm hostnames.

Neither total includes the other, so stating 58/22 as the figure after the
sweep would be stale in main the moment the second of the two PRs merges.
Says so now: the delta is what either round can claim, and the running
total is whatever the first run on main prints once both have landed.

Same rule, and the same wording, as the wiki's CI-CD page carries for the
two rounds.
2026-10-13 10:20:48 +00:00
jklein 23ac537e24 docs(rss-bridge): the green install-test reproduced, so say "twice" rather than "one run"
Validate Helm Charts / Detect changed charts (pull_request) Successful in 7s
Validate Helm Charts / Resource quantities (pull_request) Successful in 19s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 9s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 20s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m8s
Validate Helm Charts / Install-test (pull_request) Successful in 1m8s
The previous commit recorded the install-test going green and hedged it
with "it is one run". Pushing that commit produced a second run, on a
second commit, and it was green too - run 792 job 2623 (1m10s) and run 794
job 2638 (1m14s), both with the chart genuinely in the matrix.

So the hedge was understated in one direction and the real caution lies
elsewhere. Corrected to say what is actually true: it reproduced over two
consecutive runs, so it is not a fluke - and nobody has identified what
changed on the runner, which is the reason #51 stays open. A green test
does not explain itself, and the diagnostic programme in that issue's body
(docker info on the runner for userns and the dind configuration, the same
image in a local k3d without remapping) remains unexecuted.

"Close an issue on a cause, not on a colour" is unaffected by the count.
2026-10-13 10:17:42 +00:00
jklein 7d365c912c docs(rss-bridge): the install-test went green, and the chart is ruled out as the cause
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 15s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 16s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 8s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 23s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m14s
Validate Helm Charts / Install-test (pull_request) Successful in 1m14s
This chart's install-test had failed on the Gitea runner since the move
(#51): php-fpm as uid 33 unable to read /app/index.php, the startup probe
403ing through its whole 150s budget, the pod never Ready. The chart was
deliberately kept OUT of ci/excluded-charts.txt so that a pass would be
noticed rather than silenced.

It passed, in this PR. Measured from the run: k3d cluster up, helm install
in Ingress mode, pod 1/1 Ready in 22s with 0 restarts, no "Permission
denied" anywhere in the job, and all eight check contexts green.

The chart is ruled out as the cause by a diff rather than by an argument:
the rendered output for the fixture values is BYTE-IDENTICAL to
origin/main - proven in the previous commit to justify the Minor bump - so
the pod spec that succeeded is, line for line, the pod spec that had been
failing. The implicated layer is the runner, which is where #51 had
already placed the burden of proof. The control case arrived for free,
from evidence collected for an unrelated reason.

What this does NOT establish, and the docs say so: that it is repaired.
It is one run, nobody has identified what changed on the runner, and an
intermittent effect needs several runs per side. #51 therefore stays open
and is to be closed on a cause, not on a colour - which was the standing
instruction there before the green arrived.

Corrected accordingly, because shipping documentation that says the test
fails while it demonstrably passed is the half-finished state this repo
tries not to leave behind:

  - README: the httproute-test paragraph, guideline 4's closing passage on
    the last chart of the sweep, and the chart table row.
  - rss-bridge/templates/NOTES.txt: the Gateway branch no longer claims
    the chart has no successful install-test. What remains true, and is
    what it now says, is that the install-test installs the INGRESS mode,
    so the Gateway mode is render-only - the same position as all 21 other
    charts of the sweep rather than a worse one.
  - ci/httproute/rss-bridge.yaml: same correction, plus why the overlay
    stays load-bearing either way.

The wiki carries the same correction in the same round (rss-bridge, CI-CD)
plus a Lessons-Learned entry on the decision that produced the signal: an
excluded test is not a quiet test but a blind one, because you also lose
the moment the fault stops existing.
2026-10-13 10:10:28 +00:00
jklein 45b2d2ea4e feat(rss-bridge): Gateway-API as an alternative to the Ingress (last chart)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 17s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 15s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 8s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 24s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m10s
Validate Helm Charts / Install-test (pull_request) Successful in 1m10s
rss-bridge was the twenty-second and last chart of the Gateway-API sweep:
22 charts carried an Ingress template, 21 an HTTPRoute template, and the
difference was exactly this one. It had been held back as frozen by #51 -
a runner defect, not a property of the chart - and that freeze is lifted.

One HTTPRoute carrying ALL THREE hostnames, because nothing differs per
host: every name serves the same Service at "/", measured on the live
Ingress. spec.hostnames is a list, so one object is the whole answer and a
second would repeat one rule. (Unlike homeassistant, whose paths differ per
host, or matrix-synapse, whose two routes want different backends.)

httpRoute.hostnames is its own MANDATORY value, which is a third shape for
this repository and follows from the values rather than from taste. Most
charts fall back to ingress.domain; freesailarr and homeassistant reject a
hostnames key outright, because a chart-global list could not say which app
or host entry it belonged to. This chart has no ingress.domain either, but
its names are unambiguous - and the ingress.hosts list it would otherwise
reuse carries per-host tlsSecretName values that address nothing once TLS
sits on the Gateway's listener. Reusing it would drag a structure whose
other half is meaningless into the new mode.

rss-bridge.tlsGroups is therefore INERT in Gateway mode, and that is named
as a simplification rather than quietly translated: the helper exists only
to group hosts by TLS secret name, and there is nothing left to group. The
required() on ingress.hosts needed no change to become conditional - it
already sits inside the Ingress template behind ingress.enabled - so what
is new is that values.yaml and the schema now say so.

The ingress.enabled-derived-value screen that caught sure, freesailarr and
ytdlp-web-player was run here and came back CLEAN: outside the Ingress
template the only readers of an ingress value are NOTES.txt, which knows
its own mode, and tlsGroups, called only from behind the switch. The app
takes no base URL in its environment, so no "published" helper is needed.

First chart of the sweep whose Gateway branch has NO loss to explain, and
it was measured rather than assumed (read-only kubectl against the live
object): the running Ingress carries the cert-manager annotation plus Helm
and Rancher/Fleet bookkeeping, and not one nginx.ingress.kubernetes.io
annotation. The group-3 screen came back empty, so NOTES.txt says so
positively instead of copying a loss paragraph from another chart.

Also corrects the last occurrence in the repository of the outdated
sentence "charts in this repo deliberately have no dependencies"
(rss-bridge/templates/_helpers.tpl) - dependencies are admissible but
expensive since #214, and ytdlp-web-player, stalwart and paperless-ngx use
subcharts. The reason the helper is duplicated is unchanged, only its
content: sharing it would mean a library chart, a second artefact to
publish, version and pull for about one change a year.

Minor, not major: additive, defaults unchanged, and relaxing a required is
not a tightening. Proven with an EMPTY helm template diff against
origin/main for the defaults (229 lines, byte-identical); the Gateway mode
with its overlay renders the HTTPRoute in place of the Ingress and nothing
else changes. appVersion untouched - 2026-09-30 is the newest date tag the
registry has, everything newer is sha-* and latest.

ci/run-httproute-match-test.sh goes from 56 assertions over 21 charts to
58 over 22. The overlay at ci/httproute/rss-bridge.yaml has to SUPPLY
hostnames, the second in the repository that does (homeassistant's was the
first, for a different reason), because this chart has no fallback.

NOT PROVEN, and stated in the PR as well: that traffic flows. No Gateway
controller runs in CI and no HTTPRoute reaches "Accepted: True" there. For
this chart there is additionally NO successful install-test at all - it
fails on the Gitea runner in either mode (#51, reproduced on two images
while the same chart and image run healthy in the cluster). The chart is
deliberately NOT added to ci/excluded-charts.txt: an exclusion would
silence the signal, and the day the runner is fixed the test goes green by
itself. So the only red contexts expected here are
"Install-test / Install-test rss-bridge" and the collective
"Validate Helm Charts / Install-test".

Fixes #114
Fixes #40
2026-10-13 09:58:11 +00:00
8 changed files with 477 additions and 14 deletions

No files matched your search

+9 -5
View File
@@ -31,7 +31,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
| `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap. Optional `valkey` subchart — **this repo's own chart, from the published index**, off by default and gated on `valkeySubchart.enabled` (not `valkey.enabled`, which the subchart's strict schema rejects). Configured as a **queue** (PVC + `noeviction`), unlike `stalwart`'s cache: `PAPERLESS_REDIS` is the Celery broker, and `allkeys-lru` evicts across the whole keyspace, where a backlog of waiting consume tasks is exactly what LRU reads as cold. The password comes from the **same 1Password item** as the Redis connection, in a third field `auth-conf` holding the complete line `requirepass <password>` — so the password sits in that item **twice** and a rotation has to touch both fields. Two more traps: with the subchart on, `redis-connection-url` in the vault **must** be repointed at `<release>-valkey-service:6379` or the bundled Valkey runs unused, and `valkeySubchart:` must not be erased — a null override deletes the key, Helm reads an unresolvable `condition:` as *enabled*, so erasing the switch turns the subchart **on** (rejected by `values.schema.json`). Never change `valkey/` and this chart in the same PR, see "Chart dependencies" | | `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap. Optional `valkey` subchart — **this repo's own chart, from the published index**, off by default and gated on `valkeySubchart.enabled` (not `valkey.enabled`, which the subchart's strict schema rejects). Configured as a **queue** (PVC + `noeviction`), unlike `stalwart`'s cache: `PAPERLESS_REDIS` is the Celery broker, and `allkeys-lru` evicts across the whole keyspace, where a backlog of waiting consume tasks is exactly what LRU reads as cold. The password comes from the **same 1Password item** as the Redis connection, in a third field `auth-conf` holding the complete line `requirepass <password>` — so the password sits in that item **twice** and a rotation has to touch both fields. Two more traps: with the subchart on, `redis-connection-url` in the vault **must** be repointed at `<release>-valkey-service:6379` or the bundled Valkey runs unused, and `valkeySubchart:` must not be erased — a null override deletes the key, Helm reads an unresolvable `condition:` as *enabled*, so erasing the switch turns the subchart **on** (rejected by `values.schema.json`). Never change `valkey/` and this chart in the same PR, see "Chart dependencies" |
| `patchmon` | PatchMon | One Deployment (the 2.x server serves frontend and `/api/*` from one process) plus an optional guacd sidecar; OIDC-only sign-in | | `patchmon` | PatchMon | One Deployment (the 2.x server serves frontend and `/api/*` from one process) plus an optional guacd sidecar; OIDC-only sign-in |
| `rallly` | Rallly | Deployment, DB/OIDC/SMTP secrets | | `rallly` | Rallly | Deployment, DB/OIDC/SMTP secrets |
| `rss-bridge` | RSS-Bridge | Deployment + whitelist ConfigMap; `ingress.hosts` is a required list of host + optional `tlsSecretName`, so several hosts can keep their own existing certificates (documented deviation from the one-TLS-secret rule, see its `values.yaml`) | | `rss-bridge` | RSS-Bridge | Deployment + whitelist ConfigMap; `ingress.hosts` is a required list of host + optional `tlsSecretName`, so several hosts can keep their own existing certificates (documented deviation from the one-TLS-secret rule, see its `values.yaml`) — required only while `ingress.enabled`, since in Gateway mode the names come from `httpRoute.hostnames` and the secret grouping has nothing to group. **The last chart of the Gateway-API sweep (#114)**. Its install-test had failed on the Gitea runner since the move (#51) and the chart was deliberately **not** excluded so that a pass would be noticed — it passed in the conversion PR, on a render byte-identical to `main`, which rules the chart out and leaves the runner |
| `samba` | Samba | StatefulSet, users secret + config ConfigMap | | `samba` | Samba | StatefulSet, users secret + config ConfigMap |
| `satisfactory` | Satisfactory game server | StatefulSet, no ingress | | `satisfactory` | Satisfactory game server | StatefulSet, no ingress |
| `searxng` | SearXNG | Stateless Deployment + settings ConfigMap (`use_default_settings: true` plus a free-form `settings.extra` passthrough); the signing key is a required `OnePasswordItem`, an external Valkey an optional second one. `ingress.domain` is read twice — Ingress host and `SEARXNG_BASE_URL` — so it stays required with the Ingress switched off | | `searxng` | SearXNG | Stateless Deployment + settings ConfigMap (`use_default_settings: true` plus a free-form `settings.extra` passthrough); the signing key is a required `OnePasswordItem`, an external Valkey an optional second one. `ingress.domain` is read twice — Ingress host and `SEARXNG_BASE_URL` — so it stays required with the Ingress switched off |
@@ -181,7 +181,11 @@ Three things to know before touching it:
- **What is deliberately not checked**, named so the gaps are not mistaken for coverage. *That traffic flows* — no Gateway controller runs in CI and none will, so `Accepted: True` is not obtainable; that claim is simply not made anywhere in this pipeline. *That any redirect is ever served* — the pairing check proves the rendered **object** carries a `RequestRedirect` rule on the bare prefix and nothing more: whether an implementation honours the `statusCode` or declines it and fails the whole route is outside what a render can see. *Mutual exclusion with the Ingress* — each chart aborts its own render when `ingress.enabled` and `httpRoute.enabled` are both true, which is where it belongs: this test renders one configuration per chart, and a rule here would have to assume the switch is chart-global, which may not survive the multi-app charts of the sweep. *Cross-namespace and non-Service backends*, and a rule with no `backendRefs` at all (legal for a filter-only rule) — all three are **reported as "not checked", never counted as passing**. The redirect rule the pairing check demands is itself backend-less, so a chart that satisfies that check also earns one "not checked" line; both verdicts are correct at once. - **What is deliberately not checked**, named so the gaps are not mistaken for coverage. *That traffic flows* — no Gateway controller runs in CI and none will, so `Accepted: True` is not obtainable; that claim is simply not made anywhere in this pipeline. *That any redirect is ever served* — the pairing check proves the rendered **object** carries a `RequestRedirect` rule on the bare prefix and nothing more: whether an implementation honours the `statusCode` or declines it and fails the whole route is outside what a render can see. *Mutual exclusion with the Ingress* — each chart aborts its own render when `ingress.enabled` and `httpRoute.enabled` are both true, which is where it belongs: this test renders one configuration per chart, and a rule here would have to assume the switch is chart-global, which may not survive the multi-app charts of the sweep. *Cross-namespace and non-Service backends*, and a rule with no `backendRefs` at all (legal for a filter-only rule) — all three are **reported as "not checked", never counted as passing**. The redirect rule the pairing check demands is itself backend-less, so a chart that satisfies that check also earns one "not checked" line; both verdicts are correct at once.
- **Two anti-vacuum guards**, because the convention ships `httpRoute.enabled: false` and this test would otherwise be green over nothing. `ci/httproute/<chart>.yaml` is a render-only overlay applied **last** whose one job is to put a chart into Gateway mode — and a chart that *has* one and still renders no route **fails**. On top of that the run fails if **no assertion at all** held anywhere. `cyberchef` is exercised through its ordinary install-test fixture (which installs in Gateway mode, see below), every other converted chart through its overlay. For a multi-app chart the overlay also has to enter every shape the template has: `ci/httproute/freesailarr.yaml` moves one app onto a host of its own so that the shared-host route *and* the per-app route are both rendered. - **Two anti-vacuum guards**, because the convention ships `httpRoute.enabled: false` and this test would otherwise be green over nothing. `ci/httproute/<chart>.yaml` is a render-only overlay applied **last** whose one job is to put a chart into Gateway mode — and a chart that *has* one and still renders no route **fails**. On top of that the run fails if **no assertion at all** held anywhere. `cyberchef` is exercised through its ordinary install-test fixture (which installs in Gateway mode, see below), every other converted chart through its overlay. For a multi-app chart the overlay also has to enter every shape the template has: `ci/httproute/freesailarr.yaml` moves one app onto a host of its own so that the shared-host route *and* the per-app route are both rendered.
Measured in CI (run 719, the pilot): **30 charts, 4 assertions held**, whole job 13 s including checkout and the Helm setup, no cluster. Group 1 took that to 20 assertions over 10 charts and group 2 to 36 over 15; the prefix/redirect pairing of Gitea #118 adds the first check since the pilot rather than a chart, which brought it to 37 over 15 — one assertion for `freesailarr`'s `/qbittorrent`, plus one more "not checked" line for the backend-less redirect rule it demands. **Group 3 takes it to `56` assertions over `21` charts**, which is the sweep finished except for the frozen `rss-bridge`. Gitea #123 then takes it to **`58` over `21`**: `stalwart` grows a second route for its MTA-STS policy hosts, so the assertion count moves while the chart count does not — a converted chart gaining an object is the ordinary way this figure grows from here on, and the overlay has to enter the new shape for it to count (`ci/httproute/stalwart.yaml` carries an entry with a `clusterIssuer`, one without, and one on a subdomain). Group 3's 19 come from six charts and three of them contribute more than one route: `keycloak` 4 (public and admin routes), `matrix-synapse` 4 (homeserver and delegation routes), `homeassistant` 5 (two routes, three rules — its overlay is the only one in the repo that has to *supply* hostnames, since the chart has no `ingress.domain`), `bulwark` 2, `stalwart` 2, `ytdlp-web-player` 2. The fail path was exercised rather than assumed: a Service renamed in the route, a port the Service does not expose, a port left out entirely, an emptied `parentRefs` with the chart's own guard disabled, an overlay that renders no route, a run with zero assertions, and — for the pairing — both the redirect rule deleted outright and the redirect pointed at a path other than the one stripped; each produced a `FAIL` naming both sides and a non-zero exit. Measured in CI (run 719, the pilot): **30 charts, 4 assertions held**, whole job 13 s including checkout and the Helm setup, no cluster. Group 1 took that to 20 assertions over 10 charts and group 2 to 36 over 15; the prefix/redirect pairing of Gitea #118 adds the first check since the pilot rather than a chart, which brought it to 37 over 15 — one assertion for `freesailarr`'s `/qbittorrent`, plus one more "not checked" line for the backend-less redirect rule it demands. Group 3 took it to `56` assertions over `21` charts, from six charts of which three contribute more than one route: `keycloak` 4 (public and admin routes), `matrix-synapse` 4 (homeserver and delegation routes), `homeassistant` 5 (two routes, three rules — its overlay was the first in the repo that has to *supply* hostnames, since the chart has no `ingress.domain`), `bulwark` 2, `stalwart` 2, `ytdlp-web-player` 2. **`rss-bridge` finishes the sweep at `58` assertions over `22` charts** — read on this branch, which does **not** contain the realm-hostname round of Gitea #122 (measured separately at `60` over `21`, on a branch that did not contain `rss-bridge`). **Neither total includes the other, so the figure to trust from either is the delta**; the running total is whatever the first run on `main` prints once both have landed, and it should be read there rather than added up by hand. The delta here is one route, so two assertions, and its overlay is the **second** that has to supply hostnames — for a different reason than `homeassistant`'s, which is worth keeping apart. `homeassistant` has no `httpRoute.hostnames` key at all and its overlay supplies the names through `ingress.hosts`; `rss-bridge` *has* the key and its overlay fills it, because this chart has no `ingress.domain` to fall back to either but its hostnames are unambiguous (one Service, one path). **Gitea #123 then adds a second route to `stalwart`** — its MTA-STS policy hosts — for a delta of **two assertions and no new chart**, which is the first time this figure moved because a chart already in the list gained an *object* rather than because a chart joined it. Measured on a branch that **does** contain `rss-bridge` (it had landed by then) and does **not** contain the realm-hostname round of Gitea #122: `60` assertions over `22` charts — so by the delta rule above, that is the running total of everything merged at the time plus this round, and #122's four are still outstanding. Its overlay had to be extended or the second route would have been added without this test ever rendering it: `ci/httproute/stalwart.yaml` carries an entry with a `clusterIssuer`, one without, and one host on a subdomain. **The Ingress half of that feature is not covered here at all** — an overlay's one job is to switch Gateway mode on — so it sits in `ci/stalwart/test-values.yaml` instead, where the install-test has a real API server accept it.
**And in the same PR that chart's install-test went green, which is the event Gitea #51 was waiting for.** It had failed on the Gitea runner for months (uid 33 unable to read `/app/index.php`, startup probe 403 through its whole budget), and the chart was deliberately kept *out* of `ci/excluded-charts.txt` precisely so that the day it passed would be noticed rather than silenced. It passed: k3d cluster up, `helm install` in Ingress mode, pod `1/1 Ready` in 22 s with 0 restarts, and no `Permission denied` anywhere in the job. **The chart cannot be the reason, and that is measured rather than argued** — the rendered output for the fixture values is byte-identical to `origin/main`, so the pod spec that installed is exactly the one `main` produces. The implicated layer is the runner, which is where #51 had already put the burden of proof. Two cautions belong with the result: **nobody has identified what changed on the runner**, and a green test does not explain itself — it reproduced across two consecutive runs on two different commits of that PR, which is two data points rather than one, but still no cause; and the install covers **Ingress** mode, since that is what the fixture installs — so this chart's Gateway mode stays render-only, exactly like its 21 siblings rather than worse than them.
The fail path was exercised rather than assumed: a Service renamed in the route, a port the Service does not expose, a port left out entirely, an emptied `parentRefs` with the chart's own guard disabled, an overlay that renders no route, a run with zero assertions, and — for the pairing — both the redirect rule deleted outright and the redirect pointed at a path other than the one stripped; each produced a `FAIL` naming both sides and a non-zero exit.
### Install-test (PR pipeline stage 2) ### Install-test (PR pipeline stage 2)
@@ -403,7 +407,7 @@ These guidelines are **binding for every current and future chart** in this repo
- **A value, not auto-detection.** `.Capabilities.APIVersions` is unreliable at template time, and an `HTTPRoute` without the Gateway-API CRDs fails at install time — so a chart that guessed would sometimes render an object the cluster rejects and sometimes silently render nothing. - **A value, not auto-detection.** `.Capabilities.APIVersions` is unreliable at template time, and an `HTTPRoute` without the Gateway-API CRDs fails at install time — so a chart that guessed would sometimes render an object the cluster rejects and sometimes silently render nothing.
- **Mutually exclusive with `ingress.enabled`**, enforced by a `fail` that names **both** value paths. Two paths onto the same Service under one hostname is a conflict at DNS level already; a migration runs over a **second hostname** (`httpRoute.hostnames` set explicitly while the Ingress keeps `ingress.domain`), never over parallel operation. - **Mutually exclusive with `ingress.enabled`**, enforced by a `fail` that names **both** value paths. Two paths onto the same Service under one hostname is a conflict at DNS level already; a migration runs over a **second hostname** (`httpRoute.hostnames` set explicitly while the Ingress keeps `ingress.domain`), never over parallel operation.
- **`parentRefs` is mandatory in Gateway mode**, enforced by a `fail` on the empty list. A route without a parent attaches to nothing and is simply inert — created, schema-valid, rejected by nobody, reported by nothing. - **`parentRefs` is mandatory in Gateway mode**, enforced by a `fail` on the empty list. A route without a parent attaches to nothing and is simply inert — created, schema-valid, rejected by nobody, reported by nothing.
- **One source of truth for the hostname.** Empty `hostnames` means `[ingress.domain]`; two values that have to agree drift apart. `ingress.domain` is therefore read in Gateway mode too — it names the release's public hostname and is not an Ingress switch. - **One source of truth for the hostname.** Empty `hostnames` means `[ingress.domain]`; two values that have to agree drift apart. `ingress.domain` is therefore read in Gateway mode too — it names the release's public hostname and is not an Ingress switch. After the sweep there are **three** shapes here, and which one a chart gets follows from its values rather than from taste: a chart with an `ingress.domain` falls back to it (the default, most charts); a chart with no `ingress.domain` whose names could not be attributed — `freesailarr` to one of ten apps, `homeassistant` to one host entry and therefore one set of paths — has **no `hostnames` key at all** and reads the names where they already live; and `rss-bridge` has the key as a **mandatory** value with no fallback, because it has no `ingress.domain` either but its names *are* unambiguous (one Service at `/`), while the `ingress.hosts` list it would otherwise reuse carries per-host `tlsSecretName` values that address nothing once TLS sits on the listener. The rule behind all three: a hostname list is admissible exactly where it can say what it belongs to, and reusing an Ingress structure whose other half is meaningless in this mode is not a saving.
- **`NOTES.txt` says which mode is active**, as `keycloak` already does for its cases, and names what the mode costs. - **`NOTES.txt` says which mode is active**, as `keycloak` already does for its cases, and names what the mode costs.
- **What has no counterpart, and must be said rather than rebuilt.** The Gateway API has no annotation channel for controller behaviour. `rewrite-target` has a real equivalent (the `URLRewrite` filter in a rule); **`proxy-body-size` has none** — an upload limit is implementation-specific policy, so a chart that depends on one must print in its Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway. Charts that *set or recommend* one carry that disclosure: `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline` since group 2, `bulwark` and `matrix-synapse` since group 3. `garage` *depends* on one without setting it: its `ingress.annotations` comment tells the operator to set `proxy-body-size: "0"` because ingress-nginx's 1 MB default truncates S3 uploads. Measured while sweeping group 1 — "sets no behaviour-bearing annotation" is not the same question as "needs none", and the sweep screens on the second one. `freesailarr` is the mirror image of `garage` and was measured in group 2: it sets no body-size annotation and needs none itself, but `ingress.annotations` is the only lever an operator has for one (qBittorrent takes torrent-file uploads), so its Gateway-mode `NOTES.txt` says that an annotation set there is gone. The same applies to auth annotations in front of an app with no login of its own: in Gateway mode that escape hatch does not exist in the chart at all (`cyberchef`, `searxng`), and so does any annotation a chart documents as its only lever for a response header (`bentopdf`: `X-Robots-Tag`). - **What has no counterpart, and must be said rather than rebuilt.** The Gateway API has no annotation channel for controller behaviour. `rewrite-target` has a real equivalent (the `URLRewrite` filter in a rule); **`proxy-body-size` has none** — an upload limit is implementation-specific policy, so a chart that depends on one must print in its Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway. Charts that *set or recommend* one carry that disclosure: `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline` since group 2, `bulwark` and `matrix-synapse` since group 3. `garage` *depends* on one without setting it: its `ingress.annotations` comment tells the operator to set `proxy-body-size: "0"` because ingress-nginx's 1 MB default truncates S3 uploads. Measured while sweeping group 1 — "sets no behaviour-bearing annotation" is not the same question as "needs none", and the sweep screens on the second one. `freesailarr` is the mirror image of `garage` and was measured in group 2: it sets no body-size annotation and needs none itself, but `ingress.annotations` is the only lever an operator has for one (qBittorrent takes torrent-file uploads), so its Gateway-mode `NOTES.txt` says that an annotation set there is gone. The same applies to auth annotations in front of an app with no login of its own: in Gateway mode that escape hatch does not exist in the chart at all (`cyberchef`, `searxng`), and so does any annotation a chart documents as its only lever for a response header (`bentopdf`: `X-Robots-Tag`).
- **Screen the LIVE object, not only the chart — group 3's lesson, and the generalisation of Gitea #118.** A chart can read as correct while the running system fills a gap of the chart with an annotation the **operator** set, and the Gateway mode drops `ingress.annotations`, so it drops the compensation with it. At #118 that was found *after* shipping, on `freesailarr`, where the annotation was a redirect nobody would have looked for in the chart. Group 3 measured it beforehand with read-only `kubectl` against the live cluster, and **four of its six charts carried such an annotation that the chart never sets**: `keycloak` (`proxy-buffer-size: 16k` and `proxy-buffers-number: 4`, on *both* of its Ingress objects), `matrix-synapse` (`proxy-body-size: 50m`, plus the `nginx.org/client-max-body-size` twin for the other nginx controller, plus a `configuration-snippet`), `stalwart` (the whole `enable-cors` block), `bulwark` (`proxy-body-size: 25m`). None of them appears in a `git grep` of this repository, because no template sets any of them — the only hits are the *documentation* that recommends two of them. **The measurement at the live object is part of the procedure, not a flourish**, and reading the chart alone is not a substitute for it. - **Screen the LIVE object, not only the chart — group 3's lesson, and the generalisation of Gitea #118.** A chart can read as correct while the running system fills a gap of the chart with an annotation the **operator** set, and the Gateway mode drops `ingress.annotations`, so it drops the compensation with it. At #118 that was found *after* shipping, on `freesailarr`, where the annotation was a redirect nobody would have looked for in the chart. Group 3 measured it beforehand with read-only `kubectl` against the live cluster, and **four of its six charts carried such an annotation that the chart never sets**: `keycloak` (`proxy-buffer-size: 16k` and `proxy-buffers-number: 4`, on *both* of its Ingress objects), `matrix-synapse` (`proxy-body-size: 50m`, plus the `nginx.org/client-max-body-size` twin for the other nginx controller, plus a `configuration-snippet`), `stalwart` (the whole `enable-cors` block), `bulwark` (`proxy-body-size: 25m`). None of them appears in a `git grep` of this repository, because no template sets any of them — the only hits are the *documentation* that recommends two of them. **The measurement at the live object is part of the procedure, not a flourish**, and reading the chart alone is not a substitute for it.
@@ -416,11 +420,11 @@ These guidelines are **binding for every current and future chart** in this repo
- `freesailarr` (group 2), whose `freesailarr.sharedIngressApps` was gated on `ingress.enabled` and is read by the `<APP>__SERVER__URLBASE` environment variables as well as by the routing objects. In Gateway mode that list would have come out **empty**, so Radarr, Sonarr and Prowlarr would have been told no base path while their routes served them under `/radarr`, `/sonarr`, `/prowlarr` — a UI whose assets and API calls are linked at the host root, behind a Ready pod. The two guards living in that helper (seerr/mediathekarr only at `/`, no duplicate paths on the shared host) would have gone unenforced in that mode for the same reason. Helper `freesailarr.published`. - `freesailarr` (group 2), whose `freesailarr.sharedIngressApps` was gated on `ingress.enabled` and is read by the `<APP>__SERVER__URLBASE` environment variables as well as by the routing objects. In Gateway mode that list would have come out **empty**, so Radarr, Sonarr and Prowlarr would have been told no base path while their routes served them under `/radarr`, `/sonarr`, `/prowlarr` — a UI whose assets and API calls are linked at the host root, behind a Ready pod. The two guards living in that helper (seerr/mediathekarr only at `/`, no duplicate paths on the shared host) would have gone unenforced in that mode for the same reason. Helper `freesailarr.published`.
- `ytdlp-web-player` (group 3), the one this file had listed as still open, and the find with the sharpest consequence. Its `ytdlp-web-player.validate` guard — which refuses to publish the application and the bundled oauth2-proxy on the **same host** — was gated on `ingress.enabled`, so in Gateway mode it went silent. **And that guard is needed more in Gateway mode, not less.** In Ingress mode ignoring it fails loudly: ingress-nginx's admission webhook rejects the second Ingress and the Fleet bundle lands in `ErrApplied`. In Gateway mode **the webhook never fires** — it compares Ingress objects with Ingress objects, and an `HTTPRoute` is a different kind — so nothing rejects anything, both objects stay valid, and one hostname is served over two data paths that are *not* equivalent: the proxy's Ingress goes through **authentication**, the chart's `HTTPRoute` goes straight to the application. Which one a client reaches depends on what DNS resolves to, so it is an authentication bypass that comes and goes with a record, reported by nothing. Helper `ytdlp-web-player.published`, and the Gateway-mode abort carries its own message saying exactly that. - `ytdlp-web-player` (group 3), the one this file had listed as still open, and the find with the sharpest consequence. Its `ytdlp-web-player.validate` guard — which refuses to publish the application and the bundled oauth2-proxy on the **same host** — was gated on `ingress.enabled`, so in Gateway mode it went silent. **And that guard is needed more in Gateway mode, not less.** In Ingress mode ignoring it fails loudly: ingress-nginx's admission webhook rejects the second Ingress and the Fleet bundle lands in `ErrApplied`. In Gateway mode **the webhook never fires** — it compares Ingress objects with Ingress objects, and an `HTTPRoute` is a different kind — so nothing rejects anything, both objects stay valid, and one hostname is served over two data paths that are *not* equivalent: the proxy's Ingress goes through **authentication**, the chart's `HTTPRoute` goes straight to the application. Which one a client reaches depends on what DNS resolves to, so it is an authentication bypass that comes and goes with a record, reported by nothing. Helper `ytdlp-web-player.published`, and the Gateway-mode abort carries its own message saying exactly that.
All three are now closed, and the screen was run over all six charts of group 3 rather than only the suspected one. All three are now closed, and the screen was run over all six charts of group 3 rather than only the suspected one — and over `rss-bridge` as the last chart, where it came back **clean**: outside its Ingress template the only readers of an ingress value are `NOTES.txt`, which knows its own mode, and the `rss-bridge.tlsGroups` helper, which is called only from behind `ingress.enabled`. The application takes no base URL and no hostname in its environment, so there is nothing to compute from the switch and no `published` helper is needed. **A clean result is worth recording too**: the screen's value is that it is run, not that it always finds something, and "this chart needed no helper" is a measurement rather than an omission.
- **A subchart does not come along, and that has to be said where somebody will look for it.** `ytdlp-web-player`'s oauth2-proxy dependency has **no Gateway-API support** and this repo does not patch upstream charts (see "Chart dependencies"), so it renders an Ingress or nothing. In the *recommended* configuration — the proxy in the data path, owning the host, the chart's own routing object off — the object that publishes the release is therefore **the subchart's Ingress in both of the chart's modes**, and setting `httpRoute.enabled` there changes nothing about how the host is served. The chart's own route is for the proxy-disabled case or for the application on a host of its own, which is the documented unauthenticated bypass. Stated in the template header, in `values.yaml`, in the schema and in the proxy branch of `NOTES.txt`, because an operator migrating hosts would otherwise set the value and wonder why nothing moved. - **A subchart does not come along, and that has to be said where somebody will look for it.** `ytdlp-web-player`'s oauth2-proxy dependency has **no Gateway-API support** and this repo does not patch upstream charts (see "Chart dependencies"), so it renders an Ingress or nothing. In the *recommended* configuration — the proxy in the data path, owning the host, the chart's own routing object off — the object that publishes the release is therefore **the subchart's Ingress in both of the chart's modes**, and setting `httpRoute.enabled` there changes nothing about how the host is served. The chart's own route is for the proxy-disabled case or for the application on a host of its own, which is the documented unauthenticated bypass. Stated in the template header, in `values.yaml`, in the schema and in the proxy branch of `NOTES.txt`, because an operator migrating hosts would otherwise set the value and wonder why nothing moved.
- **CI**: either the chart's install-test fixture installs in Gateway mode, or it gets a render-only overlay at `ci/httproute/<chart>.yaml` — otherwise `ci/run-httproute-match-test.sh` is green over nothing for it, which that test fails on. An overlay for a multi-app chart has a second job: it must enter **every** shape the template has, or the branch nobody exercised is the one that breaks. `ci/httproute/freesailarr.yaml` gives one app a host of its own so both the shared-host route and the per-app route render. - **CI**: either the chart's install-test fixture installs in Gateway mode, or it gets a render-only overlay at `ci/httproute/<chart>.yaml` — otherwise `ci/run-httproute-match-test.sh` is green over nothing for it, which that test fails on. An overlay for a multi-app chart has a second job: it must enter **every** shape the template has, or the branch nobody exercised is the one that breaks. `ci/httproute/freesailarr.yaml` gives one app a host of its own so both the shared-host route and the per-app route render.
Reference implementation: `template-chart/templates/xxx.httproute.yaml`. Pilot: `cyberchef`. Group 1 — the charts rendering one Ingress with no behaviour-bearing annotation — followed: `bentopdf`, `garage`, `patchmon`, `rallly`, `searxng`, `sure`, `urlaubsverwaltung`, `voucher-vault`. Group 2 — the charts whose behaviour hangs off an nginx annotation — followed: `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline` (all four `proxy-body-size`) and `freesailarr` (`rewrite-target`). **Group 3 — the special shapes — finishes the sweep**: `homeassistant`, `keycloak`, `bulwark`, `matrix-synapse`, `stalwart`, `ytdlp-web-player`. That leaves exactly one chart, `rss-bridge`, which is frozen for changes (#51) and is why **#114 cannot be closed while #51 is open**. Reference implementation: `template-chart/templates/xxx.httproute.yaml`. Pilot: `cyberchef`. Group 1 — the charts rendering one Ingress with no behaviour-bearing annotation — followed: `bentopdf`, `garage`, `patchmon`, `rallly`, `searxng`, `sure`, `urlaubsverwaltung`, `voucher-vault`. Group 2 — the charts whose behaviour hangs off an nginx annotation — followed: `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline` (all four `proxy-body-size`) and `freesailarr` (`rewrite-target`). Group 3 — the special shapes: `homeassistant`, `keycloak`, `bulwark`, `matrix-synapse`, `stalwart`, `ytdlp-web-player`. **`rss-bridge` finishes the sweep**, as the twenty-second and last chart: 22 charts carried an Ingress template and 21 an HTTPRoute template, and the difference was exactly this one. It had been held back as frozen for changes by #51 — a runner defect, not a property of the chart — and the earlier claim that **#114 could not be closed while #51 was open no longer holds**: the maintainer lifted that freeze, so #114 closes with the chart converted while **#51 stays open**, since the runner defect is worked around for this purpose rather than fixed. Its shape: one `HTTPRoute` carrying **all three** of its hostnames, because nothing differs per host (every name serves the same Service at `/`, measured on the live Ingress), so `spec.hostnames` as a list is the whole answer and a second object would repeat one rule. It is also the only chart of the sweep whose Gateway branch has **no loss to document** — its live Ingress carries no `nginx.ingress.kubernetes.io` annotation at all, so the group-3 screen came back empty. Its install-test, red since the move (#51), **went green in the conversion PR** with a pod `Ready` in 22 s and no `Permission denied`; since that PR's rendered defaults are byte-identical to `origin/main`, the chart is ruled out as the cause and the runner is the implicated layer. That green covers Ingress mode, which is what the fixture installs, so the Gateway mode of this chart is render-only like every other chart's.
**Three of #114's descriptions of the group-3 shapes did not survive contact with the charts**, and the corrected list is worth having here because it is the one somebody will copy from: **Three of #114's descriptions of the group-3 shapes did not survive contact with the charts**, and the corrected list is worth having here because it is the one somebody will copy from:
+70
View File
@@ -0,0 +1,70 @@
# Render-only overlay for ci/run-httproute-match-test.sh, applied LAST.
#
# Its one job is to put this chart into Gateway mode, because the convention
# ships with httpRoute.enabled: false and the test would otherwise be green
# over nothing. A chart that has an overlay here and still renders no
# HTTPRoute FAILS the test - that is the whole point of the file existing.
#
# An overlay and not a change to ci/rss-bridge/test-values.yaml, and for this
# chart the reason is sharper than for its siblings. The install-test runs
# exactly ONE configuration per chart and the two modes are mutually exclusive,
# so switching the fixture would trade this chart's Ingress away - and that
# fixture's Ingress is the only thing that exercises rss-bridge.tlsGroups, the
# secret-grouping helper that keeps the three grown certificates of the real
# installation on their existing names. That helper is inert in Gateway mode
# (nothing to group once TLS sits on the listener), so a fixture switched to
# this mode would stop covering it entirely. The CRD schema check the pilot
# paid for is already performed by cyberchef's fixture against the real
# Gateway-API CRDs; what is missing without this file is the render-level
# assurance, and that is what this overlay buys.
#
# A note on this chart's install-test, because it has a history. It had failed
# on the Gitea runner since the move - php-fpm unable to read /app/index.php,
# the startup probe 403ing through its whole budget, the pod never Ready (Gitea
# #51), reproduced on two images while the same chart and image ran healthy in
# the cluster. The chart was deliberately kept OUT of ci/excluded-charts.txt so
# that a pass would be noticed rather than silenced, and in the PR that added
# this overlay it PASSED: pod 1/1 Ready in 22s, 0 restarts, no permission error
# in the job. Since that PR's rendered defaults are byte-identical to
# origin/main, the chart is ruled out as the cause and the runner is the
# implicated layer - which is where #51 had already placed it. It reproduced
# over two consecutive runs, but nobody has identified what changed there, so
# #51 is not closed on the strength of a colour.
#
# Either way this overlay stays load-bearing: the install-test installs the
# INGRESS mode (that is what the fixture sets), so the Gateway mode of this
# chart is covered here at render level and nowhere else - the same situation
# as every other chart in the sweep.
#
# ingress.enabled: false is not optional here - the chart ships it as TRUE and
# aborts its own render when both modes are on.
#
# httpRoute.hostnames MUST be supplied, and this overlay is the second in the
# repository that has to (homeassistant's was the first). The other charts let
# an empty hostnames list fall back to ingress.domain; this chart has no
# ingress.domain, and it deliberately does not reuse ingress.hosts from the
# fixture, because those entries carry tlsSecretName values that address
# nothing once the certificate sits on the Gateway's listener. The render
# aborts on an empty list rather than publishing a route that matches every
# request reaching its listener.
#
# THREE hostnames on ONE object, which is the shape this chart's production
# installation has (three hosts, all serving the same Service at "/"). It is
# also the shape the test should see: one HTTPRoute, one rule, one backendRef,
# several hostnames - as opposed to homeassistant's one-object-per-host or
# matrix-synapse's two objects with different paths and backends. The three
# names are kept recognisably parallel to ci/rss-bridge/test-values.yaml so the
# two modes can be read side by side, but they are NOT the same values: these
# are the route's hostnames, and the fixture's are the Ingress's hosts.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
hostnames:
- rss-bridge.ci.local
- rssbridge.ci.local
- rss-bridge.ci.example
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/RSS-Bridge/rss-bridge/master/static/logo
type: application type: application
version: 2.0.0+up2026.09.30 version: 2.1.0+up2026.09.30
appVersion: "2026-09-30" appVersion: "2026-09-30"
maintainers: maintainers:
+75 -1
View File
@@ -1,6 +1,10 @@
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
RSS-Bridge {{ .Chart.AppVersion }} was installed as release "{{ .Release.Name }}". RSS-Bridge {{ .Chart.AppVersion }} was installed as release "{{ .Release.Name }}".
{{ if .Values.ingress.enabled -}} {{ if .Values.ingress.enabled -}}
ACCESS - INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
Reachable at: Reachable at:
{{ range $entry := .Values.ingress.hosts }} https://{{ $entry.host }} {{ range $entry := .Values.ingress.hosts }} https://{{ $entry.host }}
{{ end }} {{ end }}
@@ -8,12 +12,82 @@ Each host serves the same instance. Certificates are issued by the
cert-manager cluster issuer "{{ .Values.ingress.clusterIssuer }}" into these secrets: cert-manager cluster issuer "{{ .Values.ingress.clusterIssuer }}" into these secrets:
{{ range $secret := (fromYaml (include "rss-bridge.tlsGroups" (dict "hosts" .Values.ingress.hosts "root" $))).order }} {{ $secret }} {{ range $secret := (fromYaml (include "rss-bridge.tlsGroups" (dict "hosts" .Values.ingress.hosts "root" $))).order }} {{ $secret }}
{{ end }} {{ end }}
Hosts sharing a secret name share one certificate with several SANs; hosts with
distinct names get one certificate each. That grouping is what keeps the three
grown certificates of this installation on their existing secret names.
{{- else if $httpRoute.enabled -}}
ACCESS - GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
Reachable at:
{{ range $hostnames }} https://{{ . }}
{{ end }}
All of these names are served by ONE HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute), because
nothing differs between them: one rule, PathPrefix "/", one backend. Adding or
removing a name is an edit to httpRoute.hostnames and nothing else.
This release renders that route and NOTHING else of the routing path - no
Gateway, no class, no Certificate. Three things therefore belong to the cluster
and not to this chart, and if any of them is missing the hostnames above simply
do not answer:
1. The Gateway-API CRDs. Without them this install would have failed, so
they are present - nothing to check.
2. The Gateway named in httpRoute.parentRefs
{{- range $httpRoute.parentRefs }}
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
{{- end }}
must exist AND its listener must allow routes from this namespace
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment is
visible only in the route's status:
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
Read status.parents[].conditions - "Accepted: True" and
"ResolvedRefs: True" is what a working attachment looks like. An empty
status means no controller has looked at the route at all.
3. A TLS certificate for every hostname above, on the Gateway's listener.
ingress.clusterIssuer is NOT used in this mode and is not required - the
chart renders no Certificate here, so none of the tls-* secrets listed by
the Ingress mode exists and there is nothing for cert-manager to do on
behalf of this release.
NOTHING THIS CHART DID IN INGRESS MODE IS LOST HERE
Worth stating plainly, because it is not the usual case in this repository:
most charts switched to the Gateway API give something up, since
ingress.annotations has no counterpart in this API and an operator's
annotation often carried real behaviour. This chart's live Ingress was measured
before the switch and carries no nginx.ingress.kubernetes.io annotation at all -
no body-size limit, no buffer sizes, no CORS, no rewrite, no auth. So there is
no compensation to re-create at the Gateway and no behaviour to miss.
Two things do become inert, and neither is a loss:
* ingress.annotations - gone in this mode, as everywhere. Empty here.
* The TLS secret grouping (rss-bridge.tlsGroups, and the per-host
ingress.hosts[].tlsSecretName it reads) - with the certificate on the
listener there is nothing left to group. This is the chart's most
intricate Ingress-mode machinery and it simply has no job here.
What is NOT demonstrated: that traffic actually flows. No Gateway controller
runs in CI, so no HTTPRoute of this chart has ever reached "Accepted: True"
there, and the install-test installs the Ingress mode rather than this one.
Check the route status above after the first install rather than assuming it
attached.
{{- else -}} {{- else -}}
The Ingress is disabled (ingress.enabled=false). Reach the instance with: ACCESS - NEITHER INGRESS NOR HTTPROUTE
ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate; ingress.hosts, ingress.clusterIssuer
and httpRoute.hostnames are not needed. Reach the instance with:
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
and open http://localhost:8080 and open http://localhost:8080
To publish it instead, set either ingress.enabled=true with ingress.hosts and
ingress.clusterIssuer, or httpRoute.enabled=true with httpRoute.hostnames and
httpRoute.parentRefs.
{{- end }} {{- end }}
Enabled bridges ({{ len .Values.whitelist }} entr{{ if eq (len .Values.whitelist) 1 }}y{{ else }}ies{{ end }} in whitelist.txt): Enabled bridges ({{ len .Values.whitelist }} entr{{ if eq (len .Values.whitelist) 1 }}y{{ else }}ies{{ end }} in whitelist.txt):
+19 -3
View File
@@ -15,9 +15,10 @@ Rules (variant B, see issue #19):
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as - a missing cpu limit is NOT set at all (the kubelet then treats CPU as
unlimited; CPU is compressible, so this cannot kill the workload). unlimited; CPU is compressible, so this cannot kill the workload).
NOTE: charts in this repo deliberately have no dependencies, so this helper is NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
duplicated into every chart's templates/_helpers.tpl with the chart's name as the chart's name as define prefix, because sharing it would mean a library
define prefix. template-chart holds the reference implementation. chart, and a dependency is admissible but expensive (README, guideline 14) -
this chart takes none. template-chart holds the reference implementation.
*/}} */}}
{{- define "rss-bridge.resources" -}} {{- define "rss-bridge.resources" -}}
{{- $resources := . | default dict -}} {{- $resources := . | default dict -}}
@@ -316,6 +317,21 @@ names keep one certificate each, which is what the existing installation needs
First-appearance order rather than sorted order, so the rendered tls list can First-appearance order rather than sorted order, so the rendered tls list can
be compared line by line against the manifest the chart replaces. be compared line by line against the manifest the chart replaces.
INGRESS MODE ONLY - THIS HELPER IS INERT IN GATEWAY MODE (Gitea #114)
Its entire purpose is to group hosts by TLS secret name, and in Gateway mode
there is no TLS in this chart to group: the certificate sits on the Gateway's
listener and belongs to the platform. Both of its callers - the Ingress
template and the Ingress branch of NOTES.txt - live behind ingress.enabled,
which Gateway mode forces to false, so it is simply never invoked there.
Named rather than left to be noticed, because it is the most intricate part of
this chart's Ingress and its absence from the Gateway path is a SIMPLIFICATION,
not an omission. The corollary is that ingress.hosts[].tlsSecretName has no
meaning in Gateway mode, which is why httpRoute.hostnames is a plain list of
strings instead of a reuse of ingress.hosts - see the header of
rss-bridge.httproute.yaml.
*/}} */}}
{{- define "rss-bridge.tlsGroups" -}} {{- define "rss-bridge.tlsGroups" -}}
{{- $root := .root -}} {{- $root := .root -}}
@@ -0,0 +1,141 @@
{{/*
The Gateway-API alternative to rss-bridge.ingress.yaml (Gitea #114), and the
LAST chart of that sweep: 22 charts carried an Ingress template and 21 an
HTTPRoute template, and the difference was this one. It was held back because
its install-test fails on the Gitea runner (#51), not for anything about its
shape.
WHAT THIS RENDERS, AND WHAT IT DELIBERATELY DOES NOT
Only an HTTPRoute with parentRefs. No Gateway, no gatewayClassName, no
Certificate, and no implementation-specific policy object (nginx-gateway-
fabric's ClientSettingsPolicy, Envoy Gateway's ClientTrafficPolicy or whatever
the next one calls it). That subset is the part which looks IDENTICAL on
nginx-gateway-fabric, Cilium, Envoy Gateway and Istio - keeping to it is what
lets the cluster's implementation be chosen, or changed, without touching this
chart again. A policy object here would bind the chart to one implementation
and to a CRD whose presence it cannot check.
So the class and the certificate move from this chart to the PLATFORM: they sit
on the Gateway and its listeners, which belong in cluster-config, not here.
ONE OBJECT FOR ALL THREE HOSTNAMES, BECAUSE NOTHING DIFFERS PER HOST
spec.hostnames is a LIST and its rules apply to every hostname of the object.
That is a problem for a chart whose paths differ per host, and this chart is
the opposite case: every hostname serves the SAME Service at "/" - measured on
the live Ingress, which carries three host rules with byte-identical path and
backend. So one object with three hostnames, not three objects.
Contrast, because both shapes exist in this repository and the difference is
not style: homeassistant renders one route PER host entry (its paths differ per
host), matrix-synapse renders two (homeserver and delegation want different
paths and different backends), and the keycloak realms of Gitea #122 would need
one each (app-root differs per host). Here a second object would carry the same
rule twice.
WHY httpRoute.hostnames IS ITS OWN VALUE AND DOES NOT REUSE ingress.hosts
The other charts of the sweep fall into two camps, and this chart is in
neither. Where a chart has ingress.domain - a single public name - empty
hostnames falls back to it, so there is one source of truth (cyberchef).
Where a chart has no ingress.domain, httpRoute has no hostnames key at all and
the names come from the values that already hold them, because a chart-global
list could not say which app or which host entry it belonged to (freesailarr,
homeassistant).
This chart has no ingress.domain either, but its hostnames are unambiguous:
one Service, one path, so a chart-global list means exactly one thing. What
makes reuse of ingress.hosts wrong here is the other half of those entries -
each carries an OPTIONAL tlsSecretName, which exists only to keep three grown
certificates alive on their existing secret names (see the deviation note in
values.yaml). In Gateway mode TLS sits on the Gateway's listener, so those
names address nothing. Reusing the list would carry a structure whose second
field is meaningless into the new mode and invite somebody to set it there.
Hence: httpRoute.hostnames is a plain list of strings and is MANDATORY in this
mode. There is no ingress.domain to fall back to, and an empty list would
render a route matching every request that reaches its listener - on a shared
Gateway that means answering for hostnames belonging to other releases.
AND THEREFORE rss-bridge.tlsGroups IS INERT IN THIS MODE
That helper exists for one purpose: group hosts by TLS secret name so that
hosts sharing a name share one certificate with several SANs. With the
certificate on the listener there is nothing left for it to group. It is read
by the Ingress template and by NOTES.txt's Ingress branch, both behind
ingress.enabled, so this mode simply never calls it. That is a SIMPLIFICATION
and is named as one rather than quietly translated: the most intricate part of
this chart's Ingress has no counterpart here because it has no job here.
NOTHING IS LOST WITH THE ANNOTATIONS, AND THAT IS MEASURED, NOT ASSUMED
Group 3 of the sweep screened the LIVE Ingress objects, not just the charts,
because an operator's annotation can fill a gap the chart never sets and this
mode drops ingress.annotations - four of its six charts carried exactly such an
annotation. The same screen was run here against the running object
(2026-10-13, read-only kubectl): it carries the cert-manager cluster-issuer
annotation plus Helm and Rancher/Fleet bookkeeping, and NOT ONE
nginx.ingress.kubernetes.io annotation. So this is the first chart of the sweep
whose Gateway branch in NOTES.txt has no loss to explain. ingress.annotations
is still inert in this mode - whatever a future installation puts there is
gone - but there is nothing there today to lose.
WHY A VALUE AND NOT AUTO-DETECTION
.Capabilities.APIVersions is unreliable at template time (it answers from the
cluster helm happens to be pointed at, and not at all for `helm template`),
while an HTTPRoute without the Gateway-API CRDs fails at INSTALL time. A chart
that guessed would therefore sometimes render an object the cluster rejects and
sometimes silently render nothing. So the mode is a value.
*/}}
{{- /*
Null-safe like every other block this chart dereferences (#99): an override
written without children - "httpRoute:" with the keys below it deleted - is
YAML null, and `.Values.httpRoute.enabled` would abort the render with a nil
pointer. An empty block means "chart defaults apply", and all three defaults
here are empty or false, so the whole block collapses to "no HTTPRoute".
*/ -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if and $httpRoute.enabled .Values.ingress.enabled -}}
{{- fail "ingress.enabled and httpRoute.enabled are both true - set exactly one of them. The Ingress and the HTTPRoute would publish the same Service under the same hostnames over two different data paths, which is already a conflict at DNS level: one name, two addresses, and which one a client reaches is luck. Note that ingress.enabled DEFAULTS TO TRUE in this chart, so switching to Gateway mode means setting ingress.enabled: false explicitly. A migration runs over a SECOND hostname (give httpRoute.hostnames a name the Ingress does not serve) and never over parallel operation of both." -}}
{{- end -}}
{{- if $httpRoute.enabled -}}
{{- $parentRefs := $httpRoute.parentRefs | default list -}}
{{- if not $parentRefs -}}
{{- fail "httpRoute.enabled is true but httpRoute.parentRefs is empty - an HTTPRoute without a parent attaches to no Gateway. It is created, it is schema-valid, and it routes NOTHING, with no event and no condition that says so; the only symptom is a hostname that does not answer. Name the cluster's Gateway, e.g. httpRoute.parentRefs: [{name: public, namespace: gateway}]." -}}
{{- end -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- fail "httpRoute.enabled is true but httpRoute.hostnames is empty - there is no hostname to publish. Unlike most charts here this one has NO ingress.domain to fall back to, and ingress.hosts is deliberately not reused: its entries carry tlsSecretName values that address nothing once TLS sits on the Gateway's listener. An HTTPRoute with no hostnames also does not render nothing - it matches EVERY request reaching its listener, so on a shared Gateway this release would answer for hostnames belonging to other releases. Set the public names explicitly, e.g. httpRoute.hostnames: [rss-bridge.example.com, feeds.example.com]." -}}
{{- end -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-httproute
spec:
parentRefs:
{{- toYaml $parentRefs | nindent 4 }}
hostnames:
{{- toYaml $hostnames | nindent 4 }}
rules:
{{- /*
The Ingress counterpart is `path: / , pathType: Prefix` on the one backend
Service, repeated once per host rule - RSS-Bridge is served at the root and
has no second path, so one rule covers all hostnames of this object.
The port is the Service's port NUMBER and not its name: backendRefs takes a
PortNumber, so unlike the Ingress - which references the targetPort by the
name rss-bridge-port - this reference cannot be made by name, and the 80
below has to track rss-bridge.service.yaml by hand.
ci/run-httproute-match-test.sh is what holds that pair: it fails when a
backendRef names a Service, or a port, the same render does not create.
*/}}
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port: 80
{{- end }}
+36 -4
View File
@@ -55,21 +55,23 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template (issue #93). Nothing outside the Ingress template reads an ingress value in this chart - the container takes no host in its environment - so switching it off frees every value below (guideline 1, checked rather than assumed).", "description": "Gates the whole Ingress template (issue #93). Nothing outside the Ingress template reads an ingress value in this chart - the container takes no host in its environment - so switching it off frees every value below (guideline 1, checked rather than assumed). Mutually exclusive with httpRoute.enabled (Gitea #114) - the render aborts when both are true - and the default here is TRUE, so Gateway mode needs this set to false explicitly. Re-checked for #114, the sweep in which three charts turned out to compute a value from this switch that silently flipped in Gateway mode: this chart computes none. Outside the Ingress template the only readers of an ingress value are NOTES.txt, which knows its own mode, and the tlsGroups helper, which is called only from behind this switch.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
"description": "Not read in Gateway mode: the chart renders no Certificate there. Required only while ingress.enabled, because its single read lives inside the Ingress template behind that switch.",
"type": ["string", "null"] "type": ["string", "null"]
}, },
"className": { "className": {
"description": "Not read in Gateway mode: the class is a property of the Gateway there, and the Gateway belongs to the platform.",
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.", "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. Not read in Gateway mode: the Gateway API has no annotation channel for controller behaviour. Measured on the live object (2026-10-13): the running Ingress carries no nginx.ingress.kubernetes.io annotation at all, so unlike most charts of the sweep this one loses nothing by that today - an absence of evidence about this installation, not a guarantee about another.",
"type": "object" "type": "object"
}, },
"hosts": { "hosts": {
"description": "Hosts this Ingress serves. Each entry carries a host and an optional tlsSecretName; entries sharing a secret name are grouped into one tls entry. Null is accepted by the schema so the template's required() produces the readable error instead of a schema violation.", "description": "Hosts this Ingress serves. Each entry carries a host and an optional tlsSecretName; entries sharing a secret name are grouped into one tls entry. Null is accepted by the schema so the template's required() produces the readable error instead of a schema violation. Not read in Gateway mode, where the public names come from httpRoute.hostnames instead - the required() on this list sits inside the Ingress template behind ingress.enabled and is therefore already conditional on the mode.",
"type": ["array", "null"], "type": ["array", "null"],
"items": { "items": {
"type": "object", "type": "object",
@@ -81,16 +83,46 @@
"minLength": 1 "minLength": 1
}, },
"tlsSecretName": { "tlsSecretName": {
"description": "Overrides the conventional tls-<release>-<chart>-ingress name for this host. Set only where an existing certificate must keep its secret name - see the deviation note in values.yaml.", "description": "Overrides the conventional tls-<release>-<chart>-ingress name for this host. Set only where an existing certificate must keep its secret name - see the deviation note in values.yaml. MEANINGLESS IN GATEWAY MODE, where the certificate sits on the Gateway's listener - which is why httpRoute.hostnames is a plain list of strings rather than a reuse of this list.",
"type": ["string", "null"] "type": ["string", "null"]
} }
} }
} }
} }
} }
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114), and this chart is the last of that sweep. Mutually exclusive with ingress.enabled - the render aborts when both are true, naming both value paths; note that ingress.enabled defaults to TRUE here, so this mode needs it set to false explicitly. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. ONE object carries every hostname, because nothing differs per host - all hostnames serve the same Service at '/'. The helper rss-bridge.tlsGroups is inert in this mode: with TLS on the listener there is nothing left to group by secret name. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"parentRefs": {
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": {
"$ref": "#/definitions/parentRef"
}
},
"hostnames": {
"description": "The public names of this release in Gateway mode. Required while httpRoute.enabled is true - the template aborts on an empty list, which a JSON Schema \"required\" cannot express because the key ships as an empty list. Its own value rather than a fallback or a reuse: this chart has no ingress.domain to fall back to, and ingress.hosts entries carry tlsSecretName values that address nothing once TLS sits on the Gateway's listener. Unlike freesailarr and homeassistant - which reject a hostnames key outright because a chart-global list could not say which app or host entry it belonged to - this chart serves one Service on one path, so a chart-global list is unambiguous. An empty list would not render nothing: a route without hostnames matches every request reaching its listener.",
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
}
} }
}, },
"definitions": { "definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
"type": "object",
"required": ["name"]
},
"resources": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests AND its explicit limits (issue #99). Reproducing only the requests would let limitFactor compute a memory limit below the measured peak.", "description": "May be null / empty: the chart then falls back to its default requests AND its explicit limits (issue #99). Reproducing only the requests would let limitFactor compute a memory limit below the measured peak.",
"type": ["object", "null"], "type": ["object", "null"],
+126
View File
@@ -186,15 +186,55 @@ ingress:
# takes no domain in its environment and nginx serves as default_server # takes no domain in its environment and nginx serves as default_server
# without a server_name - so switching the Ingress off frees every value # without a server_name - so switching the Ingress off frees every value
# below, and none of them needs a separate required(). # below, and none of them needs a separate required().
#
# MUTUALLY EXCLUSIVE WITH httpRoute.enabled (Gitea #114) - the render aborts
# when both are true, naming both value paths. Note the default here is TRUE,
# so switching to Gateway mode means setting this to false explicitly.
#
# Re-checked for #114, because this is the value that trips charts up in the
# Gateway mode: anything COMPUTED from ingress.enabled flips when the mode
# changes, and three charts of the sweep (sure, freesailarr,
# ytdlp-web-player) had exactly such a value - one of them a silent
# authentication bypass. This chart has none. The grep was run over the whole
# chart directory: outside the Ingress template the only readers of an
# ingress value are NOTES.txt (which knows its own mode) and the tlsGroups
# helper (called only from behind this switch). RSS-Bridge takes no base URL
# and no hostname in its environment, so there is nothing here that could
# come out wrong while the pod stayed Ready - no "published" helper needed.
enabled: true enabled: true
# NOT READ IN GATEWAY MODE: the chart renders no Certificate there, so there
# is nothing for cert-manager to do on behalf of this release. Required only
# while ingress.enabled - the single read lives inside the Ingress template,
# behind that switch.
clusterIssuer: null clusterIssuer: null
# NOT READ IN GATEWAY MODE: the class is a property of the Gateway there, and
# the Gateway belongs to the platform (cluster-config).
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
#
# NOT READ IN GATEWAY MODE: the Gateway API has no annotation channel for
# controller behaviour, so anything put here is gone in that mode.
#
# Unlike most charts of the sweep, this one loses NOTHING by that today, and
# it was measured rather than assumed (2026-10-13, read-only kubectl against
# the live object): the running Ingress carries the cert-manager
# cluster-issuer annotation plus Helm and Rancher/Fleet bookkeeping, and not
# one nginx.ingress.kubernetes.io annotation. Group 3 of the sweep found an
# operator-set annotation filling a chart's gap on four of its six charts -
# that screen came back empty here. An absence of evidence about THIS
# installation is not a guarantee about yours: whatever you put here still
# disappears in Gateway mode.
annotations: {} annotations: {}
# The hosts this Ingress serves. Required - the chart ships no default, # The hosts this Ingress serves. Required - the chart ships no default,
# because hostnames are installation data and have no business in a chart. # because hostnames are installation data and have no business in a chart.
# #
# NOT READ IN GATEWAY MODE, and this is the one value of the block where that
# needed a decision rather than a note - see httpRoute.hostnames below. The
# required() on this list lives inside the Ingress template, behind
# ingress.enabled, so it is already conditional on the mode and demands
# nothing in Gateway mode.
#
# Each entry is a host plus an OPTIONAL tlsSecretName: # Each entry is a host plus an OPTIONAL tlsSecretName:
# #
# hosts: # hosts:
@@ -223,4 +263,90 @@ ingress:
# place today and turns the eventual consolidation onto one certificate - or # place today and turns the eventual consolidation onto one certificate - or
# the retirement of the legacy hostnames - into a values change instead of a # the retirement of the legacy hostnames - into a values change instead of a
# template change (the explicit ask from cluster-config#33). # template change (the explicit ask from cluster-config#33).
#
# tlsSecretName IS MEANINGLESS IN GATEWAY MODE - the certificate sits on the
# Gateway's listener - which is why httpRoute.hostnames below is a plain list
# of strings rather than a reuse of this one.
hosts: null hosts: null
# THE GATEWAY-API ALTERNATIVE TO THE INGRESS ABOVE (Gitea #114), and this chart
# is the LAST of that sweep - 22 charts carried an Ingress template, 21 an
# HTTPRoute template, and the difference was this one. It was held back because
# its install-test fails on the Gitea runner (#51), not for anything about its
# shape, and that test is still red: the failure is a known, bounded
# environment defect and no finding about this mode.
#
# Off by default, so an existing release is unaffected. Mutually exclusive with
# ingress.enabled - the render aborts when both are true, naming both value
# paths. Note that ingress.enabled DEFAULTS TO TRUE here, so this mode needs
# ingress.enabled: false set explicitly.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform (cluster-config), which is what keeps this chart independent of
# whether the cluster runs nginx-gateway-fabric, Cilium, Envoy Gateway or
# Istio.
#
# ONE OBJECT FOR EVERY HOSTNAME, because nothing differs per host: all three
# hostnames of the live installation serve the same Service at "/", measured on
# the running Ingress. spec.hostnames is a list and its rules apply to every
# hostname of the object, so one object is enough - unlike homeassistant (paths
# differ per host) or matrix-synapse (different paths and backends).
#
# WHAT BECOMES INERT, AND IT IS A SIMPLIFICATION RATHER THAN A LOSS:
# rss-bridge.tlsGroups, the helper that groups hosts by TLS secret name so the
# three grown certificates keep their names. With TLS on the listener there is
# nothing left for it to group, and this mode never calls it.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields are the Gateway API's
# ParentReference (name, and optionally namespace, sectionName, port, group,
# kind), passed through unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# The public names of this release in Gateway mode. REQUIRED while
# httpRoute.enabled is true - the render aborts on an empty list.
#
# Its own value, and both halves of that are deliberate:
#
# Why not a fallback like the other charts: most of them have an
# ingress.domain - one public name - and an empty hostnames list falls back
# to it so there is a single source of truth. This chart has no
# ingress.domain; it has a LIST of hosts, so there is no single name to fall
# back to.
#
# Why not a reuse of ingress.hosts: those entries carry an optional
# tlsSecretName, which exists only to keep three grown certificates alive on
# their existing secret names, and which addresses nothing once TLS sits on
# the Gateway's listener. Reusing the list would drag a structure whose
# second field is meaningless into this mode and invite somebody to set it
# there. (freesailarr and homeassistant REJECT a hostnames key outright, for
# the opposite reason: a chart-global list could not say which app or which
# host entry it belonged to. Here there is one Service on one path, so a
# chart-global list says exactly one thing - which is why this chart gets the
# key those two refuse.)
#
# An empty list does NOT render nothing: an HTTPRoute without hostnames
# matches every request that reaches its listener, so on a shared Gateway
# this release would answer for hostnames belonging to other releases. Hence
# the abort.
#
# hostnames:
# - rss-bridge.example.com
# - feeds.example.com
hostnames: []