Files
helm-charts/homeassistant/templates/homeassistant.httproute.yaml
T
jklein b10800da2a feat(homeassistant): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep, and the one chart where the convention had to bend
rather than the chart. homeassistant is the DOCUMENTED INGRESS EXCEPTION
(README): upstream's flexible values, a list of hosts each with its own list
of paths, instead of the repo's required domain/clusterIssuer pair. The
exception stays - nothing here unifies it.

NO httpRoute.hostnames KEY, which is the freesailarr shape from group 2 and
for the same reason: there is 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. So httpRoute carries only enabled and
parentRefs, the hostnames come from ingress.hosts, and the chart renders ONE
HTTPRoute PER ENTRY with one rule per path of that entry. One object with
several hostnames would be wrong, not merely different: an HTTPRoute's rules
apply to every hostname of the object, and per-host paths are this chart's
whole point. The schema rejects httpRoute.hostnames outright, so the absence
is enforced rather than merely intended.

ingress.hosts is therefore read in BOTH modes and is not an Ingress-only
value. ingress.className, ingress.tls and ingress.annotations are not read in
Gateway mode: class and certificate belong to the Gateway's listener, and
there is no annotation channel.

pathType IS THE SHARP EDGE, AND THE CHART'S DEFAULT IS THE AWKWARD VALUE.
Ingress has Exact/Prefix/ImplementationSpecific; the Gateway API has
Exact/PathPrefix/RegularExpression. Exact and Prefix map cleanly.
ImplementationSpecific - the default in values.yaml - means "ask the
controller" and has NO counterpart, so:

  - on "/" it renders as PathPrefix. Every controller treats a root
    ImplementationSpecific path as a prefix match, so the chart's own default
    works in this mode unchanged (verified: the untouched default renders a
    valid route).
  - on ANY OTHER path the render ABORTS, naming the value and asking for
    Prefix or Exact. In ingress-nginx - this chart's default class - an
    ImplementationSpecific sub-path is a REGEX, and PathPrefix is not one.
    Mapping it silently would change how the path matches while the render
    stayed green, which is exactly the class of defect #118 was.

RegularExpression is deliberately not offered as an escape hatch: its support
level is Implementation-specific, which decision 1 of #114 forbids.

NOTHING WAS MEASURED ON THE LIVE CLUSTER FOR THIS CHART, because there is
nothing to measure: homeassistant is NOT DEPLOYED in jk-cluster (checked
2026-10-12 - 23 Ingress objects cluster-wide, none this chart's, and no
workload of it in any namespace). So it is the one chart of the six with no
operator-supplied annotation to lose. values.yaml and NOTES.txt say that as
an absence of evidence about this installation rather than as a property of
the chart.

Route NAMES carry the entry's INDEX, not its hostname - a hostname is not a
dependable object-name fragment. Consequence named in values.yaml and in the
template: reordering ingress.hosts renames routes and therefore recreates
rather than edits them. Harmless, since a route holds no state and no
certificate.

ci/httproute/homeassistant.yaml has to SUPPLY HOSTS, which no other overlay in
the sweep does, and it adds coverage the install-test never had: this chart's
ingress.enabled defaults to FALSE and the fixture does not switch it on, so
no routing object was exercised at all before. Two entries on purpose - a
plain one, and one with two paths including an Exact - because that second
entry is what proves the chart renders one object per host rather than one
object with several hostnames. 5 assertions over 2 routes and 3 rules.

NOTES.txt asks $published where it asked ingress.enabled, and 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.

Minor: additive, defaults unchanged. Empty helm template diff against
origin/main, both with the fixture as it stands and with ingress.enabled=true.

Part of #114
2026-10-13 08:59:09 +00:00

165 lines
10 KiB
YAML

{{/*
HTTPRoute - the Gateway-API alternative to homeassistant.ingress.yaml
(Gitea #114), AND THE ONE CHART IN THE SWEEP WHERE THE CONVENTION HAD TO BEND
RATHER THAN THE CHART.
This chart is the DOCUMENTED INGRESS EXCEPTION (README): it keeps upstream's
flexible ingress values - a LIST of hosts, each with its own LIST of paths,
plus a separate tls list - instead of the repository's required
domain/clusterIssuer pair. The exception stays. Nothing here unifies it, and
the Gateway support is built on top of it rather than next to it.
THIS CHART HAS NO ingress.domain, SO httpRoute HAS NO hostnames KEY
That is the freesailarr precedent from group 2, and the reason is the same: a
chart-global hostname list could not say which host entry - and therefore which
set of paths - it belonged to. So the hostnames come from the values that
already hold them, ingress.hosts[].host, and httpRoute carries only `enabled`
and `parentRefs`. One HTTPRoute PER HOST ENTRY, with one rule per path of that
entry, because an HTTPRoute's rules apply to every hostname of the object and
this chart's whole point is that the paths may differ per host.
ingress.hosts IS READ IN BOTH MODES, then, and it is not an "Ingress" value any
more than ingress.domain is in the other charts - it is where this release's
public names live. ingress.tls and ingress.className are NOT read here: the
certificate and the class sit on the Gateway and its listeners and belong to
the platform (decision 1 of #114). ingress.annotations is inert - the Gateway
API has no annotation channel.
Nothing was measured on the live cluster for this chart, because there is
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). So unlike its five siblings in this group it
carries no operator-supplied annotation that this mode would drop. That is an
absence of evidence about THIS installation, not a demonstration that the chart
needs none: its ingress.annotations default is {} and the chart sets nothing
itself, so whatever a future installation puts there would be dropped in this
mode exactly as elsewhere.
pathType HAS NO CLEAN COUNTERPART, AND THE CHART'S DEFAULT IS THE AWKWARD ONE
Ingress pathType has three values; the Gateway API's path match types are
Exact, PathPrefix and RegularExpression. The mapping:
Exact -> Exact
Prefix -> PathPrefix
ImplementationSpecific -> NOTHING. It means "ask the controller", so there
is no value that preserves it.
And ImplementationSpecific is this chart's DEFAULT (values.yaml,
ingress.hosts[0].paths[0].pathType). Two rules therefore, and the split is not
tidiness:
- On the path "/", ImplementationSpecific is rendered as PathPrefix. Every
controller treats a root ImplementationSpecific path as a prefix match, so
the two coincide and the chart's default works in this mode out of the box.
- On ANY OTHER path, ImplementationSpecific ABORTS THE RENDER. In
ingress-nginx - the class this chart defaults to - an
ImplementationSpecific non-root path is a REGEX, and PathPrefix is not a
regex. Mapping it silently would change matching behaviour while the render
stayed green, which is precisely the class of defect #118 was: a chart that
looks translated and routes differently. So the operator is asked to say
which of Exact or Prefix was meant.
RegularExpression is deliberately NOT offered as an escape hatch: its support
level is Implementation-specific, which would bind this chart to one Gateway
implementation and is what decision 1 of #114 forbids.
*/}}
{{- /*
Null-safe like every other block a template here dereferences (#99): an
override written without children - "httpRoute:" with its keys deleted - is
YAML null, and .Values.httpRoute.enabled would abort with a nil pointer.
*/ -}}
{{- $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 HTTPRoutes 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. For Home Assistant there is a second edge: it refuses requests whose Host header is not in its own trusted/allowed list 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. A migration runs over a DIFFERENT hostname - add a second entry to ingress.hosts and switch the modes once it answers - 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 -}}
{{- $hosts := .Values.ingress.hosts | default list -}}
{{- if not $hosts -}}
{{- fail "httpRoute.enabled is true but ingress.hosts is empty - there is no hostname to publish and the release would render no route at all, silently. This chart has no ingress.domain (it is the documented ingress exception and keeps upstream's flexible host list), so ingress.hosts is where its public names live and it is read in BOTH modes. Set at least one entry, e.g. ingress.hosts: [{host: ha.example.com, paths: [{path: /, pathType: Prefix}]}]." -}}
{{- end -}}
{{- $first := true -}}
{{- range $i, $entry := $hosts }}
{{- $host := required "Every entry of ingress.hosts needs a host - an HTTPRoute with an empty hostname matches every request that reaches its listener, which on a shared Gateway means this release answers for hostnames belonging to other releases" $entry.host -}}
{{- $paths := $entry.paths | default list -}}
{{- if not $paths -}}
{{- fail (printf "ingress.hosts[%d] (%s) has no paths - the HTTPRoute would carry no rules, and a route with no rules matches nothing while being perfectly valid and Accepted. Give it at least one, e.g. paths: [{path: /, pathType: Prefix}]" $i $host) -}}
{{- end -}}
{{- if not $first }}
---
{{- end }}
{{- $first = false -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
{{- /*
ONE ROUTE PER HOST ENTRY, so the name has to carry the entry's identity. The
index and not the hostname: a hostname is not a legal object-name fragment
(dots are legal in a name but a 63-character label limit and the leading
"<release>-<chart>-" prefix are not something a hostname can be trusted to
fit), and a sanitised hostname would collide for two hosts differing only in
a character that sanitising removes. The index is stable for a given values
file, and REORDERING ingress.hosts THEREFORE RENAMES ROUTES - which deletes
and recreates them on the next upgrade rather than editing them. Harmless
here (a route carries no state and no certificate), and named so it is not a
surprise.
*/}}
name: "{{ $.Release.Name }}-{{ $.Chart.Name }}-httproute{{ if $i }}-{{ $i }}{{ end }}"
labels:
app: "{{ $.Release.Name }}-{{ $.Chart.Name }}"
spec:
parentRefs:
{{- toYaml $parentRefs | nindent 4 }}
hostnames:
- {{ $host | quote }}
rules:
{{- range $paths }}
{{- /*
pathType mapping, with the rule from the header: ImplementationSpecific -
this chart's default - becomes PathPrefix on "/" only, and aborts the
render on any other path rather than silently changing how it matches. An
absent pathType is treated the same way; note that pathType is a REQUIRED
field of networking.k8s.io/v1 Ingress, so an entry without it renders an
Ingress the API server rejects - a pre-existing gap of the Ingress
template, not something this one introduces.
*/}}
{{- $given := .pathType | default "ImplementationSpecific" -}}
{{- $path := required "Every entry of ingress.hosts[].paths needs a path" .path -}}
{{- $type := "" -}}
{{- if eq $given "Exact" -}}
{{- $type = "Exact" -}}
{{- else if eq $given "Prefix" -}}
{{- $type = "PathPrefix" -}}
{{- else if eq $given "ImplementationSpecific" -}}
{{- if eq $path "/" -}}
{{- $type = "PathPrefix" -}}
{{- else -}}
{{- fail (printf "ingress.hosts[%d] (%s) has a path %q with pathType ImplementationSpecific, which has NO counterpart in the Gateway API - it means \"ask the controller\", and the Gateway API offers only Exact, PathPrefix and RegularExpression. On \"/\" the chart maps it to PathPrefix because every controller agrees there, but on a sub-path ImplementationSpecific is a REGEX in ingress-nginx (this chart's default class) and PathPrefix is not one, so mapping it would change how the path matches while the render stayed green. Say which you mean: pathType: Prefix or pathType: Exact. RegularExpression is deliberately not offered - its support level is Implementation-specific and would bind this chart to one Gateway implementation (decision 1 of Gitea #114)" $i $host $path) -}}
{{- end -}}
{{- else -}}
{{- fail (printf "ingress.hosts[%d] (%s) has a path %q with an unknown pathType %q - Ingress knows Exact, Prefix and ImplementationSpecific" $i $host $path $given) -}}
{{- end }}
- matches:
- path:
type: {{ $type }}
value: {{ $path | quote }}
backendRefs:
{{- /*
The port is the Service's port NUMBER and not its name "http":
backendRefs takes a PortNumber, so unlike the Ingress this reference
cannot be made by name. It comes from service.port, the same value
the Service renders, so the two cannot drift;
ci/run-httproute-match-test.sh holds the pair for the case where they
do.
*/}}
- name: "{{ $.Release.Name }}-{{ $.Chart.Name }}-service"
port: {{ $.Values.service.port | int64 }}
{{- end }}
{{- end }}
{{- end }}