diff --git a/homeassistant.md b/homeassistant.md index 2c6bb74..8a6acf3 100644 --- a/homeassistant.md +++ b/homeassistant.md @@ -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 get certificate ``` +### 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 — `-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