Files
helm-charts/matrix-synapse/values.schema.json
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

280 lines
12 KiB
JSON

{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "matrix-synapse values",
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key that the chart does not read is rejected at render time instead of being silently ignored. The only objects that stay open are the ones passed verbatim into the Kubernetes pod spec (probes, security contexts, global) - the chart does not interpret their contents, so it cannot judge which keys are meaningful. The hardening, probe and resources blocks of BOTH workloads additionally accept null, because an erased block falls back to the chart defaults (issue #99).",
"type": "object",
"additionalProperties": false,
"properties": {
"global": {
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"type": "object"
},
"scaleDown": {
"type": "boolean"
},
"secrets": {
"type": "object",
"additionalProperties": false,
"properties": {
"configSecretRef": {
"description": "op:// item path for homeserver.yaml. Changing the item CONTENT does not restart Synapse - Helm never sees it, so it cannot be hashed (#82); restart the StatefulSet by hand.",
"type": ["string", "null"]
}
}
},
"matrix": {
"type": "object",
"additionalProperties": false,
"properties": {
"replicas": {
"type": "integer",
"minimum": 0
},
"serverName": {
"type": ["string", "null"]
},
"timezone": {
"type": ["string", "null"]
},
"securityContext": {
"$ref": "#/definitions/securityContext"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"env": {
"$ref": "#/definitions/env"
},
"resources": {
"$ref": "#/definitions/resources"
},
"delegation": {
"description": "The delegation nginx Deployment, which serves the /.well-known/matrix/* endpoints. Disabled by default.",
"type": "object",
"additionalProperties": false,
"properties": {
"replicas": {
"type": "integer",
"minimum": 0
},
"enabled": {
"type": "boolean"
},
"serverUrl": {
"type": ["string", "null"]
},
"image": {
"description": "Auxiliary image, pinned independently of appVersion (which tracks Synapse).",
"type": "object",
"additionalProperties": false,
"properties": {
"repository": {
"type": ["string", "null"]
},
"tag": {
"type": ["string", "number", "null"]
}
}
},
"securityContext": {
"$ref": "#/definitions/securityContext"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
}
}
}
}
},
"storage": {
"description": "The data PVC. Not null-safe on purpose: an erased block yields an invalid PVC rather than a silently degraded workload.",
"type": "object",
"additionalProperties": false,
"properties": {
"amount": {
"type": ["string", "null"]
},
"storageClass": {
"type": ["string", "null"]
}
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"description": "Gates the whole Ingress template (issue #93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. false omits the Ingress and makes clusterIssuer unnecessary - but NOT domain, which the delegation nginx also serves in its /.well-known/matrix/* endpoints and which the HTTPRoute reads as its hostname.",
"type": "boolean"
},
"domain": {
"description": "The homeserver hostname. Read by the Ingress, by the HTTPRoute (when httpRoute.hostnames is empty) and by the delegation nginx, which serves it inside /.well-known/matrix/client - so it is required in every mode while delegation is on.",
"type": ["string", "null"]
},
"clusterIssuer": {
"type": ["string", "null"]
},
"className": {
"type": ["string", "null"]
},
"annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. INERT IN GATEWAY MODE: the Gateway API has no annotation channel. This installation's live Ingress carries proxy-body-size 50m (which has no counterpart and must be re-established at the Gateway) and a configuration-snippet of more_set_headers directives (which must NOT be re-established - see values.yaml, httpRoute block).",
"type": "object"
}
}
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114). Mutually exclusive with ingress.enabled - the render aborts when both are true, naming both value paths. The chart renders only HTTPRoutes with parentRefs; class and certificate sit on the Gateway and belong to the platform. TWO routes, not one: the Ingress is a single object with two host rules (homeserver at /, delegation at /.well-known/matrix), and an HTTPRoute's hostnames apply to the whole object, so two hostnames wanting different paths and backends must be two objects. NOT COVERED IN THIS MODE: proxy-body-size, which this installation carries at 50m and which Synapse's own max_upload_size has to agree with - see values.yaml, httpRoute block, including why the configuration-snippet on the live object is NOT a loss. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"parentRefs": {
"description": "The Gateway(s) the homeserver route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": { "$ref": "#/definitions/parentRef" }
},
"hostnames": {
"description": "Empty means [ingress.domain], so the homeserver hostname has a single source of truth. Does NOT cover the delegation host - see httpRoute.delegation.hostnames.",
"type": "array",
"items": { "type": "string" }
},
"delegation": {
"description": "The delegation route, rendered while matrix.delegation.enabled is true - the same switch the Ingress's second host rule uses.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"parentRefs": {
"description": "Empty falls back to httpRoute.parentRefs. Worth setting when the server name is published by a different Gateway than the homeserver host, which is the normal case - the server name is an apex domain somebody else's website usually owns.",
"type": "array",
"items": { "$ref": "#/definitions/parentRef" }
},
"hostnames": {
"description": "Empty means [matrix.delegation.serverUrl]. 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 hostname, and the migration order depends on telling them apart.",
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
},
"definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
"type": "object",
"required": ["name"]
},
"resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"limitFactor": {
"type": "number",
"exclusiveMinimum": 0
},
"requests": {
"$ref": "#/definitions/resourceList"
},
"limits": {
"$ref": "#/definitions/resourceList"
}
}
},
"resourceList": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"cpu": {
"type": ["string", "number", "null"]
},
"memory": {
"type": ["string", "number", "null"]
},
"ephemeral-storage": {
"type": ["string", "number", "null"]
},
"ephemeralStorage": {
"type": ["string", "number", "null"]
}
}
},
"env": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"valueFrom": {
"type": "object",
"additionalProperties": false,
"properties": {
"secretKeyRef": {
"$ref": "#/definitions/keyRef"
},
"configMapKeyRef": {
"$ref": "#/definitions/keyRef"
}
}
}
}
}
},
"keyRef": {
"type": "object",
"additionalProperties": false,
"required": ["name", "key"],
"properties": {
"name": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"securityContext": {
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99) instead of rendering an unhardened workload.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"pod": {
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
"type": ["object", "null"]
},
"container": {
"description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.",
"type": ["object", "null"]
}
}
},
"probe": {
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing.",
"type": ["object", "null"]
}
}
}