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
199 lines
11 KiB
YAML
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 }}
|