Files
jklein 9625d8b182 feat(bulwark): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep. One route, one rule, at the root - and THREE
EXPECTATIONS ABOUT THIS CHART DID NOT SURVIVE CONTACT WITH IT:

  - #114 says bulwark has TWO Ingress objects. It has ONE. keycloak has two.
  - #114's measured table lists bulwark under rewrite-target. It has no
    rewrite-target annotation and CANNOT have one: NEXT_PUBLIC_BASE_PATH is
    compiled into the bundle, so the application cannot be served under a
    sub-path, and stripping a prefix at the proxy would yield a page that
    loads and then fetches everything from the wrong place. Every occurrence
    of the string "rewrite-target" in this chart - values.yaml, the schema,
    the Ingress template - is an explanation of exactly that. A grep for the
    string finds the refutations and counts them as occurrences.
  - ci/run-httproute-match-test.sh said in its header that bulwark was the
    second rewrite-target chart and would meet the #118 prefix/redirect
    assurance when group 3 landed. Group 3 landed and it does not: with no
    URLRewrite on a prefix other than "/", the assurance's precondition is
    never met here. The header is corrected rather than left standing, and it
    now says freesailarr is the only chart the pairing applies to.

So no redirect rule is added, and none is needed.

WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT rather than inferred. On
2026-10-12 the live Ingress on webmail.bekemeyer-klein.de carried

    nginx.ingress.kubernetes.io/proxy-body-size: 25m

supplied by the OPERATOR through ingress.annotations - values.yaml recommends
exactly that value and explains why the chart cannot pick it, so the chart
sets nothing and a reader of this repository alone would not find it. It is
the single annotation this installation depends on, and the Gateway API has no
body-size counterpart: implementation policy, not routing. Per decisions 1 and
2 of #114 it is NAMED, not rebuilt - in values.yaml, in the schema and in the
Gateway branch of NOTES.txt, in this application's own terms: an attachment
above the Gateway's default is rejected before Bulwark sees a byte, the 413
comes from the proxy, and it arrives after the user has written the mail and
attached the file.

CORRECTED CLAIM, the same one garage and calibre-web-automated needed: the
Ingress template, the bulwark.publicUrl helper and the schema all stated that
nothing outside the Ingress template FUNCTIONALLY reads ingress.domain. The
HTTPRoute reads it, so the claim held only while both modes were off. All
three now say so, and the route carries a required() of its own.

bulwark.publicUrl follows the ACTIVE mode instead of always printing
ingress.domain. The two differ exactly when httpRoute.hostnames is set, which
is how a migration off the Ingress runs - and printing the old host there
would hand the operator an OIDC redirect URI for the hostname being migrated
away from, a failure that surfaces as a rejected login at the identity
provider and says nothing about this chart.

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

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

78 lines
3.8 KiB
YAML

{{- /*
ON BY DEFAULT (ingress.enabled: true): a webmail client nobody can reach is not
a webmail client.
AT THE ROOT OF THE HOST, AND THERE IS NO VALUE TO PUT IT UNDER A PATH.
NEXT_PUBLIC_BASE_PATH is compiled into the bundle - upstream: "These are
compiled into the bundle and cannot be changed at runtime" - so a sub-path
deployment needs a rebuilt image, which is outside what this repository does
(same limit as garage #81 and keycloak #83). A rewrite-target annotation is not a
way around it either: the bundle's own asset and API URLs are built for the root,
so stripping a prefix at the proxy produces a page that loads and then fetches
everything from the wrong place. The rule below is therefore "/" with pathType
Prefix, deliberately and permanently.
GUIDELINE 1, CHECKED IN BOTH DIRECTIONS (README, "Values that must not lie"),
AND FOR THIS CHART THE GATE FREES BOTH VALUES. Checked rather than assumed:
Bulwark derives its own public URL from the request origin and takes no
base-URL environment variable - there is no KC_HOSTNAME equivalent here - so a
release with no routing object at all is complete without a domain. That is the
calibre-web-automated case rather than the matrix-synapse one.
CORRECTED WITH THE GATEWAY-API SUPPORT (Gitea #114): this paragraph used to say
that nothing outside this template FUNCTIONALLY reads ingress.domain.
bulwark.httproute.yaml now does - as the route's hostname, with a required() of
its own - so the value is mandatory in Gateway mode too unless
httpRoute.hostnames is given explicitly. "The gate frees both values" means
BOTH gates off, not this one alone.
THE TWO MODES ARE MUTUALLY EXCLUSIVE and the HTTPRoute template aborts the
render when ingress.enabled and httpRoute.enabled are both true, naming both
value paths.
The one other READER is NOTES.txt, which prints the access URL and the OIDC
redirect URI. That read is deliberately NOT guarded with a required(): `helm
template` does not render NOTES.txt, so a guard there would fire for the first
time during a real `helm install` and never in lint, in the render test or in CI.
The bulwark.publicUrl helper renders a visible placeholder instead, and the
enforcement lives here - in the template that is reached by every render with the
Ingress on, which is the default.
*/ -}}
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
annotations:
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
{{- /* Merged, not replaced. The one annotation worth adding here for this
application is nginx.ingress.kubernetes.io/proxy-body-size: attachment
uploads and the Files view go through this Ingress, and nginx's
default of 1m rejects anything larger with a 413 that reads like an
application fault. The chart does not set it because the right number
is the mail server's limit, which this chart does not know. */}}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
http:
paths:
- backend:
service:
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port:
# BY NAME, so bulwark.port and this file cannot drift apart.
name: http
path: /
pathType: Prefix
tls:
- hosts:
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- end }}