Files
helm-charts/matrix-synapse/templates/matrix-synapse.httproute.yaml
T
jklein 470b90a393 feat(matrix-synapse): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep. TWO HTTPRoutes - and the shape is not the one the
issue described.

#114 says this chart renders its Ingresses in a LOOP OVER SEVERAL APPS. It
does not; that is freesailarr. It renders ONE Ingress object with TWO HOST
rules: the homeserver on ingress.domain at "/", and the delegation nginx on
matrix.delegation.serverUrl at "/.well-known/matrix". An HTTPRoute's
spec.hostnames applies to the WHOLE object, so two hostnames that want
different paths and different backends have to be two objects. That is the
only faithful translation, not a stylistic choice.

httpRoute.delegation.hostnames deliberately does NOT fall back to
httpRoute.hostnames. The server name is what REMOTE homeservers resolve and is
a different thing from the homeserver's own hostname; the migration order -
homeserver host first, server name last - depends on telling them apart, and a
chart-global hostname list covering both would erase the distinction.
httpRoute.delegation.parentRefs falls back to httpRoute.parentRefs, which
matters because the server name is usually an apex domain behind a different
Gateway.

ONE THING THIS MODE IMPROVES, and it is the awkward part of Matrix delegation.
Under ingress-nginx two Ingress objects cannot both claim one hostname - the
admission webhook rejects the second - which is why the delegation rule has to
live inside this chart's single Ingress object even though the apex domain
usually belongs to a website. HTTPRoutes attached to the same listener for the
same hostname are merged by the implementation, longest match winning, so the
/.well-known/matrix route can sit beside whatever owns that apex.

WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT ON 2026-10-12 - and ONE OF
THE TWO IS NOT WHAT IT LOOKS LIKE. The live Ingress carried, all of it
operator-supplied through ingress.annotations (this chart sets none of it):

    nginx.ingress.kubernetes.io/proxy-body-size:    50m
    nginx.org/client-max-body-size:                 50m
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Forwarded-For $remote_addr";
      more_set_headers "X-Forwarded-Proto $scheme";
      more_set_headers "Host $host:$server_port";

1. THE BODY SIZE IS THE REAL LOSS. Synapse's media repository takes uploads;
   its own ceiling is max_upload_size in homeserver.yaml, which this chart
   cannot see because the whole config arrives from 1Password. The effective
   limit is the smaller of the two, and in this mode the proxy half becomes
   the GATEWAY'S default - neither number anyone can point at is necessarily
   the one that decides. No counterpart in the Gateway API, so per decision 2
   of #114 it is documented, not rebuilt. nginx.org/client-max-body-size is
   the same limit spelled for NGINX Inc's controller, a hedge across both
   implementations, and it goes for the same reason. (#114's own measured
   table did not list matrix-synapse under proxy-body-size at all; the live
   object has it.)

2. THE SNIPPET IS NOT LOAD-BEARING, which took checking rather than assuming.
   It looks like what Synapse's x_forwarded setting depends on - client IP for
   rate limiting, and federation. It is not: more_set_headers is the
   headers-more module's OUTPUT-header directive ("Replaces (if any) or adds
   (if not any) the specified output headers"), the request-side directive
   being more_set_input_headers. So those three headers are added to the
   RESPONSE TO THE CLIENT; Synapse never sees them, and what it reads with
   x_forwarded: true is the REQUEST's X-Forwarded-For, which ingress-nginx
   supplies itself. The Gateway API's ResponseHeaderModifier could not rebuild
   it anyway (it knows no nginx variables), and should not: NOTES.txt says in
   as many words not to re-create it and not to read its absence as the cause
   of a rate-limiting or federation problem.

   Stated as what it is - a reading of the directive's documented semantics.
   Nothing here observed a Synapse instance with and without the snippet.

NOTES.txt now asks $published (ingress.enabled OR httpRoute.enabled) wherever
it used to ask ingress.enabled - the server-name/serverUrl mismatch warning,
the "clients work fine" aside and the scaled-to-zero paragraph were all really
asking whether anything publishes the release.

Minor: additive, defaults unchanged. Empty helm template diff against
origin/main with ci/matrix-synapse/test-values.yaml.

Part of #114
2026-10-12 20:55:55 +00:00

199 lines
11 KiB
YAML

{{/*
HTTPRoute - the Gateway-API alternative to matrix-synapse.ingress.yaml
(Gitea #114).
TWO ROUTES, AND THE SHAPE IS NOT WHAT #114 EXPECTED. The issue describes this
chart as rendering "its Ingresses in a loop over several apps". It does not -
that is freesailarr. This chart renders ONE Ingress object with TWO host rules:
ingress.domain / synapse service
matrix.delegation.serverUrl /.well-known/matrix delegation service
In the Gateway API that has to become TWO HTTPRoute objects, and not because of
style: spec.hostnames applies to the WHOLE object, so one route can carry
several hostnames only when they want the SAME rules. These two want different
paths and different backends, so one object per hostname is the only faithful
translation.
WHAT THIS MODE IMPROVES, WHICH IS WORTH KNOWING BEFORE THE MIGRATION
The delegation hostname is the Matrix SERVER NAME - the apex domain, not the
homeserver's own host - and it is almost always already serving something else
(a website at /). Under ingress-nginx two Ingress objects on one hostname are a
fight: the admission webhook rejects the second claim on the same host and
path, which is why the delegation rule has to live INSIDE this chart's single
Ingress object rather than beside it. HTTPRoutes do not work that way: several
routes may attach to the same listener for the same hostname and the
implementation merges them by match specificity (a longer path wins). So in
this mode the /.well-known/matrix route can sit next to whatever else owns that
apex, without either side knowing about the other.
WHAT THIS RENDERS, AND WHAT IT MUST NOT
Only HTTPRoutes with parentRefs. No Gateway, no gatewayClassName, no
Certificate, no implementation-specific policy object - the subset that looks
identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio (decision 1
of #114). Class and certificate belong to the PLATFORM, on the Gateway and its
listeners, in cluster-config. Note that the two hostnames need certificates on
TWO listeners (or one wildcard listener covering both), which in Ingress mode
were two entries in the one object's tls block.
WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT - AND ONE OF THE TWO IS NOT
WHAT IT LOOKS LIKE
On 2026-10-12 the live Ingress (jk-cluster, namespace matrix-synapse) carried,
all of it OPERATOR-supplied through ingress.annotations - this chart sets none
of it:
nginx.ingress.kubernetes.io/proxy-body-size: 50m
nginx.org/client-max-body-size: 50m
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Forwarded-For $remote_addr";
more_set_headers "X-Forwarded-Proto $scheme";
more_set_headers "Host $host:$server_port";
1. THE BODY SIZE IS THE REAL LOSS. Synapse's media repository takes uploads,
its own ceiling is max_upload_size in homeserver.yaml - which this chart
cannot see, because the whole config arrives from 1Password through
secrets.configSecretRef - and the effective limit is the smaller of the two.
In this mode the proxy half becomes THE GATEWAY'S default, so neither number
anyone can point at is necessarily the one that decides. There is no
counterpart in the Gateway API: body size is implementation policy, not
routing, and decision 2 of #114 says it is documented rather than rebuilt.
(nginx.org/client-max-body-size is the same limit spelled for the OTHER
nginx ingress controller, NGINX Inc's; it is a hedge across both
implementations and it disappears for the same reason.)
2. THE SNIPPET IS NOT LOAD-BEARING, AND THAT TOOK CHECKING RATHER THAN
ASSUMING. It looks like the thing Synapse's x_forwarded setting depends on -
client IP for rate limiting, and federation. It is not, because
more_set_headers is the module's OUTPUT-header directive:
headers-more-nginx-module, verbatim - more_set_headers "Replaces (if any) or
adds (if not any) the specified OUTPUT headers", while the request-side
directive is a different one, more_set_input_headers ("it operates on input
headers (or request headers)"). So these three headers are added to the
RESPONSE TO THE CLIENT. Synapse never sees them; what Synapse reads with
x_forwarded: true is the REQUEST's X-Forwarded-For, which ingress-nginx
populates itself.
The honest statement for the migration is therefore: not translatable, and
not needed. The Gateway API's ResponseHeaderModifier could set static
response headers but knows no nginx variables ($remote_addr, $scheme,
$host), so a rebuild is impossible anyway - and pointless, since dropping
them changes nothing Synapse depends on. What does change is that a client
stops receiving three headers echoing the proxy's view of its own request,
which is at best noise.
This is a claim about a directive's documented semantics, not an experiment:
nothing here observed a Synapse instance with and without the snippet.
Both are named in the Gateway branch of NOTES.txt, the first as a limit to
re-establish at the Gateway and the second as a thing NOT to re-establish.
*/}}
{{- /*
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 HTTPRoute would publish the same Services 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 Matrix it is worse than for a stateless app: the /.well-known/matrix delegation tells every OTHER homeserver in the federation where to find this one, and a server name that resolves to two data paths makes federation succeed or fail per remote server and per retry. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both - and the delegation host moves last, after the homeserver host answers." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain]. Two values that have to agree drift apart. ingress.domain is
read outside the routing templates in this chart - the delegation nginx serves
it inside /.well-known/matrix/client - so it already carries a required() of
its own there and stays mandatory with the Ingress off (README, guideline 12,
point 1). This required() is the route's own, so the message names the route.
*/ -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list (required "A Domain/Host must be provided: set ingress.domain (used as the homeserver HTTPRoute's single hostname when httpRoute.hostnames is empty) or set httpRoute.hostnames explicitly" .Values.ingress.domain) -}}
{{- 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 Synapse
Service. 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. ci/run-httproute-match-test.sh is what holds the
pair - it fails when a backendRef names a Service, or a port, that the
same render does not create.
*/}}
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port: 80
{{- if .Values.matrix.delegation.enabled }}
---
{{- /*
The delegation route, on the SERVER NAME rather than on the homeserver's host.
Gated on matrix.delegation.enabled exactly as the second host rule of the
Ingress is, and for the same reason: without the delegation workload there is
nothing to route to.
Its hostname deliberately does NOT fall back to httpRoute.hostnames. The
delegation host is matrix.delegation.serverUrl - the Matrix server name, which
is a different thing from the homeserver's hostname and is what remote servers
resolve. Letting a chart-global hostname list cover it would make the two
indistinguishable, and the migration order (homeserver host first, server name
last) depends on telling them apart. httpRoute.delegation.hostnames exists for
the one case where the server name itself has to move.
*/ -}}
{{- $delegation := $httpRoute.delegation | default dict -}}
{{- $delegationHostnames := $delegation.hostnames | default list -}}
{{- if not $delegationHostnames -}}
{{- $delegationHostnames = list (required "If delegation is enabled you must provide a serverUrl: set matrix.delegation.serverUrl (used as the delegation HTTPRoute's single hostname when httpRoute.delegation.hostnames is empty) or set httpRoute.delegation.hostnames explicitly" .Values.matrix.delegation.serverUrl) -}}
{{- end -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-httproute
spec:
{{- /*
Its own parent, falling back to the homeserver route's. The server name may
well be published by a different Gateway than the homeserver host - it is a
domain somebody else's website usually owns - and in that case this is where
it is said.
*/}}
parentRefs:
{{- toYaml ($delegation.parentRefs | default $parentRefs) | nindent 4 }}
hostnames:
{{- toYaml $delegationHostnames | nindent 4 }}
rules:
{{- /*
PathPrefix and not Exact, and the same path the Ingress uses: the
delegation nginx serves two documents below it
(/.well-known/matrix/server and /.well-known/matrix/client), so the
prefix has to carry both. NO URLRewrite filter - the delegation nginx's
own config answers on the full path, so nothing is stripped, which is
also why the #118 prefix/redirect assurance has no precondition to meet
here.
*/}}
- matches:
- path:
type: PathPrefix
value: /.well-known/matrix
backendRefs:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-service
port: 80
{{- end }}
{{- end }}