ADR 0020 — Deploy ingress (host / ingress_class / proxy_buffering) + app content_root
Ports two ee-pages/EE deployment improvements into the tessera deploy and web lenses.
Context
Ingress. ee-pages' assistant streams SSE per token, but the deployed
ingress-nginx coalesces the whole response into one burst before the client
sees it. EE fixed this with a per-environment deploy-DSL field
proxy_buffering: "on" | "off" (commit f93a04c96): it renders the
nginx.ingress.kubernetes.io/proxy-buffering annotation into the environment's
values, and the chart's ingress template picks it up. Unset keeps the nginx
default (buffering on, still honoring a response's X-Accel-Buffering: no).
The parser rejects any value other than on / off.
Mosaic's tessera deploy specs (ADR 0010) render a per-spec Helm chart, but that chart has no ingress at all — no host, no class, no annotations. (The legacy v0 deploy lens predates tessera and already has host / ingress_class / proxy-body-size; this ADR does not touch it.)
content_root. A portal/content app whose main page is app content cannot
own the deployment root under EE: with a home path set, the generated Leptos
router emits a GET / redirect (and a page claiming / is routed at /),
which collides with any block that mounts its own GET / (commit b2abba3e6,
"content_root — let the deployment root belong to app content"). EE added an
app-spec field content_root: bool (default false, byte-identical behavior
when omitted): when true, the Leptos factory emits no root route at all — the
root is free for the content route, and the UI keeps all its other routes and
becomes a hidden entry point.
Mosaic's web lens is the equivalent surface: the generated Leptos router
unconditionally claims the root (<Route path=() view=pages::Index/>), and
the axum fallback serves the shell for unknown paths. A content app (e.g.
mosaic-pages' doc site, P8) that wants to serve its content at / has no way
to give the root up.
Decision
1. Tessera deploy specs gain three optional ingress fields (on each
deploy { name: … } spec, so every environment can differ — the spec name is
the environment, per ADR 0010):
app:
deploys:
- name: prod
target: gcp-gke
host: pages.example.com
ingress_class: nginx
proxy_buffering: off # on | off (streaming / SSE surfaces)
host— the Ingress rule host. Unset → the rule is hostless (ClusterIP / port-forward usage).ingress_class—spec.ingressClassName. Unset → cluster default.proxy_buffering— renders thenginx.ingress.kubernetes.io/proxy-bufferingannotation. Fail closed: any value other thanon/offis abad-deploy-proxy-bufferingerror and the spec is skipped (same hard-error discipline as the store fields).
2. The per-spec Helm chart gains an ingress template.
deploy/<name>/helm/templates/ingress.yaml renders an Ingress from
values.yaml:
ingress:
host: pages.example.com # "" when unset
className: nginx # absent when unset
proxyBuffering: off # "" when unset
The annotation renders only when proxyBuffering is set (the nginx controller
key; other controllers ignore it). The chart otherwise stays minimal — TLS is
out of scope (cert-manager is cluster policy, like in EE).
3. App-level content_root: bool (default false) on app::
app:
name: pages
content_root: true
When true, the web lens omits the root route from the generated Leptos router
(path=()). Everything else is unchanged: the other pages keep their routes
(the UI remains reachable, e.g. /commands, /aggregates, /docs), and the
root belongs to whatever mounts it. This is the web-lens port of EE's
suppression; the static site lens (zola) is unaffected (it is a separate
surface with its own /), and the app lens is pure API (no root route).
The decision is stamped once in the plan (AppPlan.content_root); the render
just reads it.
Consequences
DeploySpecPlangrowshost,ingress_class,proxy_buffering; the values.yaml and the new ingress template change only for specs that set them (hostless ingress renders-with no host, exactly as the legacy v0 lens does).- Conformance goldens for any example with a tessera deploy spec are re-pinned
(the chart gains
ingress:values + the template). content_rootis a render-time fact: no data-model change, no migration. Apps that never set it render byte-identical output.- Out of scope (deliberately): TLS/cert-manager wiring, path prefixes, the
EE auto-deploy trigger / mono-SHA tagging (GitLab-mono specific), and
ui_prefix(unmerged upstream).