homeassistant: the Gateway API as an alternative to the Ingress (#114)
The chart where the convention bent rather than the chart. It is the documented ingress exception and keeps upstream's flexible host list, so the Gateway support is built on top of that block: NO httpRoute.hostnames key (the schema rejects it, so the absence is enforced), hostnames and paths from ingress.hosts in BOTH modes, and ONE HTTPRoute per entry with one rule per path - because an HTTPRoute's rules apply to every hostname of the object and per-host paths are this chart's whole point. Same shape as freesailarr, same reason. pathType gets a table, because this chart's default is the awkward value. Exact and Prefix map cleanly; ImplementationSpecific has no counterpart, is rendered as PathPrefix on "/" only - so the chart's own default works unchanged - and ABORTS the render on any other path, because in ingress-nginx an ImplementationSpecific sub-path is a regex and PathPrefix is not one. RegularExpression is deliberately not offered. Route names carry the entry's index, so reordering ingress.hosts renames and therefore recreates routes - harmless, and recorded so it is not read as a bug. And the honest negative: nothing was measured on a live object for this chart, because homeassistant is not deployed in jk-cluster at all. Unlike four of its five siblings in the group it had no operator-supplied annotation to lose - written as an absence of evidence about that installation rather than as a property of the chart. The trusted_proxies section now warns that moving from an Ingress to a Gateway changes the proxy's pod network: a list carried over unchanged turns every request into a 400 if the CIDR differs, or silently keeps attributing every client to one address if it does not.
1 file changed
+66
-11
+66
-11
@@ -8,7 +8,7 @@ Deploys [Home Assistant](https://www.home-assistant.io/) as a single-replica Sta
|
||||
| Versions | not listed on purpose — read the current pair off `homeassistant/Chart.yaml`, see [Chart index](Charts) |
|
||||
| Workloads | StatefulSet (1 replica) |
|
||||
| Storage | one RWO PVC mounted at `/config` (subPath `hass-config`) |
|
||||
| Ingress | optional and **off by default**; upstream-style `hosts`/`tls` lists, **no required values** |
|
||||
| Routing | **two mutually exclusive modes**, both **off by default**: Ingress (`ingress.enabled`) or Gateway API (`httpRoute.enabled`). Upstream-style `hosts`/`tls` lists, **no required values** — and because there is no `ingress.domain`, there is **no `httpRoute.hostnames`** either: `ingress.hosts` is read in both modes and Gateway mode renders **one route per entry**. See [Routing](#routing-ingress-or-gateway-api) |
|
||||
| External dependencies | none by default (recorder uses SQLite on the PVC unless configured otherwise) |
|
||||
| Secrets | 1 × `OnePasswordItem` (three fields) |
|
||||
| Install-test | `ci/homeassistant/` (config-secret fixture, `INSTALL_TIMEOUT=600`) |
|
||||
@@ -112,15 +112,26 @@ All three are `subPath` mounts from a Secret volume, which makes them **read-onl
|
||||
|
||||
### `ingress`
|
||||
|
||||
Documented exception to [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api): this chart keeps the upstream-style flexible block. There is no `ingress.domain`, no `ingress.clusterIssuer`, and **no required value here at all** — only `className` follows the repo standard.
|
||||
Documented exception to [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api): this chart keeps the upstream-style flexible block. There is no `ingress.domain`, no `ingress.clusterIssuer`, and **no required value here at all** — only `className` follows the repo standard. **The exception extends to the Gateway mode**, which is built on top of this block rather than beside it — see [Routing](#routing-ingress-or-gateway-api).
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `ingress.enabled` | bool | `false` | Off by default. Nothing else in the chart reads `ingress.hosts`, so no other setting silently depends on it. |
|
||||
| `ingress.className` | string | `nginx` | Set to `""` to omit `ingressClassName` from the rendered Ingress entirely. |
|
||||
| `ingress.annotations` | object | `{}` | Rendered verbatim. **This is where the cert-manager annotation has to go** — the chart adds none. |
|
||||
| `ingress.hosts` | list | `[{host: chart-example.local, paths: [{path: /, pathType: ImplementationSpecific}]}]` | Host/path list, passed verbatim into the Ingress rules. The default is the chart scaffold's **placeholder** — see [below](#the-placeholder-host). |
|
||||
| `ingress.tls` | list | `[]` | TLS list (`secretName` + `hosts`), passed verbatim. Empty means the Ingress carries no TLS section at all and the URLs are plain `http`. |
|
||||
| `ingress.enabled` | bool | `false` | Off by default. **Mutually exclusive with `httpRoute.enabled`.** |
|
||||
| `ingress.className` | string | `nginx` | Set to `""` to omit `ingressClassName` from the rendered Ingress entirely. **Not read in Gateway mode** — the class is a property of the Gateway there. |
|
||||
| `ingress.annotations` | object | `{}` | Rendered verbatim. **This is where the cert-manager annotation has to go** — the chart adds none. **Not read in Gateway mode**: the Gateway API has no annotation channel, so anything here is simply gone. |
|
||||
| `ingress.hosts` | list | `[{host: chart-example.local, paths: [{path: /, pathType: ImplementationSpecific}]}]` | **The public names of this release, and read in BOTH modes.** Host/path list, passed verbatim into the Ingress rules; in Gateway mode the chart renders **one `HTTPRoute` per entry** with one rule per path of that entry. The default is the chart scaffold's **placeholder** — see [below](#the-placeholder-host). `pathType` does not map cleanly; see [Routing](#routing-ingress-or-gateway-api). |
|
||||
| `ingress.tls` | list | `[]` | TLS list (`secretName` + `hosts`), passed verbatim. Empty means the Ingress carries no TLS section at all and the URLs are plain `http`. **Not read in Gateway mode**: the certificate sits on the Gateway's listener. |
|
||||
|
||||
### `httpRoute`
|
||||
|
||||
The Gateway-API alternative to the Ingress, **off by default**. Added with group 3 of the Gateway-API sweep (Gitea #114) — and this is the chart where the convention had to bend rather than the chart, so it has **two keys, not three**.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `httpRoute.enabled` | bool | `false` | Renders one `HTTPRoute` per entry of `ingress.hosts` instead of the Ingress. **Mutually exclusive with `ingress.enabled`** — with both `true` the render aborts naming both values. |
|
||||
| `httpRoute.parentRefs` | list | `[]` | The Gateway(s) **every** route of this release attaches to — Gateway API `ParentReference` entries (`name`, and optionally `namespace`, `sectionName`, `port`, `group`, `kind`), passed through unchanged. **Required while `httpRoute.enabled` is true**: the render aborts on an empty list. The same list is used for every route; the hosts differ, the Gateway does not. The schema requires `name` on every entry and is otherwise left open, because Kubernetes owns that schema and adds fields to it across releases. |
|
||||
|
||||
**There is deliberately no `httpRoute.hostnames`,** and the schema rejects it outright so the absence is enforced rather than merely intended. This chart has no `ingress.domain` to fall back to, and a chart-global hostname list could not say which host entry — and therefore which set of paths — it belonged to. [freesailarr](freesailarr) is the other chart without that key, for the same reason.
|
||||
|
||||
### `securityContext`
|
||||
|
||||
@@ -185,18 +196,47 @@ Unlike every other chart in this repository, this one builds **no** `cert-manage
|
||||
kubectl -n <namespace> get certificate <your ingress.tls secretName>
|
||||
```
|
||||
|
||||
### Routing: Ingress or Gateway API
|
||||
|
||||
Two mutually exclusive modes, and **both ship off** — this chart publishes nothing until one is switched on. The Gateway support is built on top of the flexible `ingress` block rather than beside it, which has three consequences.
|
||||
|
||||
**1. No `httpRoute.hostnames`, and one route per host entry.** The hostnames and the paths come from `ingress.hosts`, so that list is read in *both* modes and is not an Ingress-only value. Gateway mode renders **one `HTTPRoute` per entry**, with one rule per path of the entry. One object carrying several hostnames would be wrong rather than merely different: an `HTTPRoute`'s rules apply to **every** hostname of the object, and per-host paths are this chart's whole point.
|
||||
|
||||
Route names carry the entry's **index**, not its hostname — `<release>-homeassistant-httproute` for the first, `…-httproute-1`, `…-httproute-2` for the rest. A hostname is not a dependable object-name fragment (the 63-character label limit plus the release prefix, and a sanitised hostname can collide). **Reordering `ingress.hosts` therefore renames routes**, which deletes and recreates them on the next upgrade instead of editing them. Harmless — a route holds no state and no certificate — but worth knowing before it looks like a bug.
|
||||
|
||||
**2. `pathType` has no clean counterpart, and this chart's default is the awkward value.** Ingress offers `Exact`, `Prefix` and `ImplementationSpecific`; the Gateway API offers `Exact`, `PathPrefix` and `RegularExpression`.
|
||||
|
||||
| `pathType` | Rendered as |
|
||||
|---|---|
|
||||
| `Exact` | `Exact` |
|
||||
| `Prefix` | `PathPrefix` |
|
||||
| `ImplementationSpecific` on path `/` | `PathPrefix` — every controller treats a root `ImplementationSpecific` path as a prefix match, so the two coincide and **the chart's own default works in Gateway mode unchanged** |
|
||||
| `ImplementationSpecific` on any other path | **the render aborts**, naming the value |
|
||||
|
||||
The abort is the point rather than an inconvenience: in ingress-nginx — the class this chart defaults to — an `ImplementationSpecific` sub-path is a **regex**, and `PathPrefix` is not a regex. Mapping it silently would change how the path matches while the render stayed green, which is precisely the class of defect [#118](freesailarr) was. Say which you mean: `pathType: Prefix` or `pathType: Exact`. `RegularExpression` is deliberately **not** offered as an escape hatch — its support level is Implementation-specific, which would bind this chart to one Gateway implementation.
|
||||
|
||||
**3. Two render aborts guard the list itself.** `ingress.hosts` empty while `httpRoute.enabled` is true aborts (there would be no hostname and no route at all, silently); an entry without a `host` aborts (an `HTTPRoute` with an empty hostname matches **every** request reaching its listener, which on a shared Gateway means this release answers for hostnames belonging to other releases); and an entry without `paths` aborts (a route with no rules matches nothing while being perfectly valid and `Accepted`).
|
||||
|
||||
Plus the two rules every chart has: the modes are mutually exclusive with a `fail` naming **both** value paths — and for Home Assistant there is a second edge, since it refuses requests whose `Host` header is not in its own configuration and builds absolute URLs from it, so a client arriving over the other path can be rejected by the *application* rather than by the proxy — and `httpRoute.parentRefs` must not be empty, because a route without a parent attaches to no Gateway and routes nothing with no event and no condition to say so.
|
||||
|
||||
### Nothing was measured on a live object for this chart
|
||||
|
||||
Unlike its five siblings in group 3, **four of which carried an operator-supplied annotation that Gateway mode drops**, this chart had nothing to measure: **homeassistant is not deployed in `jk-cluster`** (checked 2026-10-12 — 23 Ingress objects cluster-wide, none of them this chart's, and no workload of it in any namespace).
|
||||
|
||||
That is an absence of evidence about *that* installation, **not a property of the chart**. `ingress.annotations` defaults to `{}` and the chart sets nothing itself, so whatever a future installation puts there would be dropped in Gateway mode exactly as elsewhere — and for this chart that includes the cert-manager annotation, which is the only way a certificate is ever requested in Ingress mode ([the Ingress exception](#the-ingress-exception-nothing-here-requests-a-certificate)).
|
||||
|
||||
### The placeholder host
|
||||
|
||||
`ingress.hosts` defaults to the scaffold entry `chart-example.local`. With `ingress.enabled: true` and that default untouched, the Ingress exists, ingress-nginx accepts it, and no request ever arrives — the name resolves nowhere. Set the host you actually serve before looking for a deeper reason.
|
||||
`ingress.hosts` defaults to the scaffold entry `chart-example.local`. With either mode switched on and that default untouched, the routing object exists, the controller or Gateway accepts it, and no request ever arrives — the name resolves nowhere. Set the host you actually serve before looking for a deeper reason. `ingress.hosts` is read in **both** modes, so this applies to Gateway mode too.
|
||||
|
||||
### Behind an Ingress, the `http` block decides between 400 and wrong client IPs
|
||||
### Behind a proxy, the `http` block decides between 400 and wrong client IPs
|
||||
|
||||
Every request through ingress-nginx arrives from the controller's **pod** IP, not from the browser. The `http-config` field is where that is dealt with, and both ways of getting it wrong are quiet in their own way:
|
||||
Every request through ingress-nginx arrives from the controller's **pod** IP, not from the browser — and in Gateway mode from the Gateway's **data-plane** pod IP instead. The `http-config` field is where that is dealt with, and both ways of getting it wrong are quiet in their own way:
|
||||
|
||||
- `use_x_forwarded_for: true` **without** the proxy's address in `trusted_proxies` makes Home Assistant reject the request outright — the browser gets `400 Bad Request` and the log names the untrusted address it saw.
|
||||
- Neither setting at all means every client looks like the ingress controller. Nothing fails; you get one address in the logbook for everyone, and the login-attempt ban then locks out all users at once because they share it.
|
||||
|
||||
The value belongs to the ingress controller's **pod network**, not to the nodes and not to the clients:
|
||||
The value belongs to the proxy's **pod network**, not to the nodes and not to the clients:
|
||||
|
||||
```yaml
|
||||
use_x_forwarded_for: true
|
||||
@@ -204,6 +244,8 @@ trusted_proxies:
|
||||
- 10.42.0.0/16 # the cluster's pod CIDR — check yours
|
||||
```
|
||||
|
||||
**Switching from an Ingress to a Gateway changes that network**, because a Gateway's data plane normally runs in a namespace of its own. A `trusted_proxies` list carried over unchanged turns **every** request into a `400` if the CIDR differs — or, if it does not, silently keeps attributing every client to one address. This is the half of a mode switch that lives in the application's own configuration, which arrives from 1Password and which no chart value and no render test can see.
|
||||
|
||||
Read the address Home Assistant actually sees out of the log rather than guessing:
|
||||
|
||||
```bash
|
||||
@@ -273,6 +315,19 @@ This is the only chart in the repository with a `templates/tests/` directory, bu
|
||||
|
||||
`ci/homeassistant/` holds `test-values.yaml` (op:// ref against the stub CRD, plus `core.storage.storageClass: local-path` because the values default `default` does not exist in k3d), `fixtures.yaml` (the config Secret the operator would otherwise materialize, with the three keys and a `trusted_proxies` entry for the k3d pod CIDR) and `settings.env` (`INSTALL_TIMEOUT=600`, because the first boot installs and initializes integrations). Home Assistant starts as root, so the writability of the dynamically provisioned volume is not an issue here — unlike the charts that need hostPath fixtures. The install-test runs on every PR that touches the chart — see [CI/CD](CI-CD).
|
||||
|
||||
**Neither routing object is installed**, and that follows from the chart rather than from the fixture: `ingress.enabled` ships `false` and the fixture does not switch it on, so the install-test has never exercised a routing object for this chart. Same situation as [garage](garage).
|
||||
|
||||
**The Gateway mode is held at render level**, by `ci/httproute/homeassistant.yaml` plus `ci/run-httproute-match-test.sh`, which runs over every chart on every PR ([CI/CD](CI-CD#httproute-render-test)) — so for this chart the overlay **adds** coverage that never existed rather than trading one object for another.
|
||||
|
||||
It is also **the only overlay in the repository that has to supply hostnames**, because this chart has no `ingress.domain` and the chart's default entry is the `chart-example.local` placeholder. Two entries, each earning its place:
|
||||
|
||||
1. `ha.ci.local` with `/` and `pathType: Prefix` — the ordinary case and the clean `Prefix` → `PathPrefix` mapping.
|
||||
2. `ha-alt.ci.local` with **two** paths, one `Prefix` and one `Exact` — the part that cannot be expressed in a single `HTTPRoute` together with entry 1, because an `HTTPRoute`'s rules apply to every hostname of the object. It is therefore the test that the chart really renders **one object per host entry** rather than one object with several hostnames, and it exercises the `Exact` mapping and the multi-rule path at once.
|
||||
|
||||
**5 assertions over 2 routes and 3 rules** — a `parentRefs` check per route plus a `backendRefs` check per rule, each held against the Service of the same render **by port number**, because `backendRefs` takes a `PortNumber` and cannot name the port `http` the way the Ingress does.
|
||||
|
||||
**What that does not cover**: that either hostname answers (no Gateway controller runs in CI, so no route reaches `Accepted: True`); the `pathType: ImplementationSpecific` **abort** on a non-root path, which is a render-*failure* path while this test only runs renders it expects to succeed — it was exercised by hand instead; and anything about whether Home Assistant accepts the `Host` header or reads `X-Forwarded-For`, both of which live in the application's own configuration and change with the proxy's address. **Nothing in CI reads `NOTES.txt`** either: `helm template` does not emit it and no script under `ci/` reads it, so its text carries review-only assurance.
|
||||
|
||||
## See also
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — §4 and §5 both name this chart as a documented exception
|
||||
|
||||
Reference in new issue
Block a user