feat: Gateway-API as an alternative to the Ingress in the six special shapes (group 3) #120
No files matched your search
+1
-1
@@ -21,7 +21,7 @@ type: application
|
|||||||
# duplicated here. The short version: eight tag shapes, six of them things
|
# duplicated here. The short version: eight tag shapes, six of them things
|
||||||
# nobody should pin, and plain `docker` versioning excludes all six - so that
|
# nobody should pin, and plain `docker` versioning excludes all six - so that
|
||||||
# block deliberately carries NO versioningTemplate.
|
# block deliberately carries NO versioningTemplate.
|
||||||
version: 1.0.0+up1.12.0
|
version: 1.1.0+up1.12.0
|
||||||
appVersion: "1.12.0"
|
appVersion: "1.12.0"
|
||||||
|
|
||||||
maintainers:
|
maintainers:
|
||||||
|
|||||||
+87
-10
@@ -1,4 +1,16 @@
|
|||||||
|
{{- /*
|
||||||
|
NOTHING IN CI READS THIS FILE. `helm template` does not emit notes and no
|
||||||
|
workflow or script under ci/ reads it, so every claim below carries
|
||||||
|
review-only assurance - it is checked by being read, not by a stage. A
|
||||||
|
rendering ERROR here does fail `helm template`, which is all the pipeline can
|
||||||
|
see of it.
|
||||||
|
*/ -}}
|
||||||
{{- $settings := fromYaml (include "bulwark.settings" .) -}}
|
{{- $settings := fromYaml (include "bulwark.settings" .) -}}
|
||||||
|
{{- $httpRoute := .Values.httpRoute | default dict -}}
|
||||||
|
{{- $hostnames := $httpRoute.hostnames | default list -}}
|
||||||
|
{{- if not $hostnames -}}
|
||||||
|
{{- $hostnames = list .Values.ingress.domain -}}
|
||||||
|
{{- end -}}
|
||||||
{{- $replicas := int (include "bulwark.replicas" (dict "scaleDown" .Values.scaleDown "replicas" $settings.replicas)) -}}
|
{{- $replicas := int (include "bulwark.replicas" (dict "scaleDown" .Values.scaleDown "replicas" $settings.replicas)) -}}
|
||||||
{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}}
|
{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}}
|
||||||
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
||||||
@@ -11,17 +23,75 @@ The messages and the accounts belong to the Stalwart server at
|
|||||||
If that server is away, this release is still Ready and still green - see
|
If that server is away, this release is still Ready and still green - see
|
||||||
"WHAT THE PROBES DO NOT TELL YOU" below.
|
"WHAT THE PROBES DO NOT TELL YOU" below.
|
||||||
|
|
||||||
ENDPOINTS
|
ENDPOINTS - {{ if .Values.ingress.enabled }}INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false){{ else if $httpRoute.enabled }}GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false){{ else }}NEITHER INGRESS NOR HTTPROUTE{{ end }}
|
||||||
{{ if .Values.ingress.enabled }} Web: {{ $url }}
|
{{ if or .Values.ingress.enabled $httpRoute.enabled }} Web: {{ $url }}
|
||||||
{{ else }} Web: no Ingress (ingress.enabled: false) - in-cluster only
|
{{ else }} Web: no Ingress and no HTTPRoute (ingress.enabled and httpRoute.enabled are
|
||||||
|
both false) - in-cluster only
|
||||||
{{ end }} In-cluster: http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local:{{ $settings.port | int64 }}
|
{{ end }} In-cluster: http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local:{{ $settings.port | int64 }}
|
||||||
{{ if .Values.ingress.enabled }} Admin: {{ $url }}/admin/login
|
{{ if or .Values.ingress.enabled $httpRoute.enabled }} Admin: {{ $url }}/admin/login
|
||||||
{{ else }} Admin: /admin/login on the in-cluster address above. With no Ingress, reach it with
|
{{ else }} Admin: /admin/login on the in-cluster address above. With nothing published, reach it with
|
||||||
kubectl -n {{ .Release.Namespace }} port-forward statefulset/{{ $sfs }} 3000:{{ $settings.port | int64 }}
|
kubectl -n {{ .Release.Namespace }} port-forward statefulset/{{ $sfs }} 3000:{{ $settings.port | int64 }}
|
||||||
and open http://localhost:3000/admin/login
|
and open http://localhost:3000/admin/login
|
||||||
{{ end }}{{ if .Values.ingress.enabled }}
|
{{ end }}{{ if or .Values.ingress.enabled $httpRoute.enabled }}
|
||||||
Bulwark is served at the ROOT of that host and cannot be moved under a path:
|
Bulwark is served at the ROOT of that host and cannot be moved under a path:
|
||||||
NEXT_PUBLIC_BASE_PATH is compiled into the image.
|
NEXT_PUBLIC_BASE_PATH is compiled into the image.{{ if $httpRoute.enabled }} That is also why the
|
||||||
|
HTTPRoute carries no URLRewrite filter - there is no prefix to strip.{{ end }}
|
||||||
|
{{ end }}{{ if $httpRoute.enabled }}
|
||||||
|
GATEWAY MODE - WHAT THIS CHART DOES NOT RENDER, AND WHAT YOU MUST SET YOURSELF
|
||||||
|
|
||||||
|
This release renders ONE HTTPRoute and NOTHING else of the routing path - no
|
||||||
|
Gateway, no class, no Certificate, no policy object.
|
||||||
|
|
||||||
|
{{ .Release.Name }}-{{ .Chart.Name }}-httproute {{ join ", " $hostnames }}
|
||||||
|
|
||||||
|
Three things therefore belong to the cluster, and if any is missing the
|
||||||
|
hostname above simply does not answer:
|
||||||
|
|
||||||
|
1. The Gateway-API CRDs. Without them this install would have failed, so
|
||||||
|
they are present - nothing to check.
|
||||||
|
2. The Gateway named in httpRoute.parentRefs
|
||||||
|
{{- range $httpRoute.parentRefs }}
|
||||||
|
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
|
||||||
|
{{- end }}
|
||||||
|
must exist AND its listener must allow routes from this namespace
|
||||||
|
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment is visible only
|
||||||
|
in the route's status:
|
||||||
|
|
||||||
|
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
|
||||||
|
|
||||||
|
Read status.parents[].conditions - "Accepted: True" and
|
||||||
|
"ResolvedRefs: True" is what a working attachment looks like. An empty
|
||||||
|
status means no controller has looked at the route at all.
|
||||||
|
3. The TLS certificate for the hostname, on the Gateway's listener.
|
||||||
|
ingress.clusterIssuer is NOT used in this mode and is not required: the
|
||||||
|
chart renders no Certificate here, so there is no
|
||||||
|
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do
|
||||||
|
for this release.
|
||||||
|
|
||||||
|
THE UPLOAD LIMIT IS NOT ENFORCED BY ANYTHING THIS CHART RENDERS.
|
||||||
|
|
||||||
|
In Ingress mode an operator sets nginx.ingress.kubernetes.io/proxy-body-size
|
||||||
|
through ingress.annotations - this installation runs with 25m - because
|
||||||
|
nginx's default of 1m rejects almost every attachment. IN THIS MODE THAT IS
|
||||||
|
GONE: the Gateway API has no body-size field, it is implementation policy
|
||||||
|
rather than routing, and this chart renders no policy object on purpose (it
|
||||||
|
would bind the chart to one Gateway implementation).
|
||||||
|
|
||||||
|
So the limit in force is THE GATEWAY'S, whatever it happens to be, and no
|
||||||
|
value of this chart has any say in it. The failure mode on a webmail client
|
||||||
|
is worth picturing before it happens: a user has written the mail, attached
|
||||||
|
the file and pressed send, and the 413 comes back FROM THE PROXY. It reads
|
||||||
|
like Bulwark being broken, nothing mentions a limit, and the draft is the
|
||||||
|
thing at risk.
|
||||||
|
|
||||||
|
Set the equivalent next to the Gateway, in your implementation's own object,
|
||||||
|
BEFORE you point DNS at it - and size it to what the MAIL SERVER accepts
|
||||||
|
(the Stalwart release above), not to what looks generous here. A webmail
|
||||||
|
client that accepts a 40 MB attachment the mail server will refuse has only
|
||||||
|
moved the failure later.
|
||||||
|
|
||||||
|
ingress.annotations is inert in this mode in general: the Gateway API has no
|
||||||
|
annotation channel for controller behaviour.
|
||||||
{{ end }}
|
{{ end }}
|
||||||
THE 1PASSWORD ITEMS, AND WHICH FIELDS THEY MUST CARRY
|
THE 1PASSWORD ITEMS, AND WHICH FIELDS THEY MUST CARRY
|
||||||
The operator materializes one Secret key per item FIELD. A missing or misnamed
|
The operator materializes one Secret key per item FIELD. A missing or misnamed
|
||||||
@@ -244,9 +314,16 @@ WHAT THIS CHART DOES NOT RENDER
|
|||||||
- No PodDisruptionBudget: with one replica a minAvailable of 1 would forbid
|
- No PodDisruptionBudget: with one replica a minAvailable of 1 would forbid
|
||||||
every voluntary eviction and make `kubectl drain` hang forever on a pod
|
every voluntary eviction and make `kubectl drain` hang forever on a pod
|
||||||
nothing may move (README, guideline 11).
|
nothing may move (README, guideline 11).
|
||||||
- No sub-path Ingress, and no value that could add one - the base path is
|
- No sub-path Ingress and no sub-path route, and no value that could add
|
||||||
compiled into the image.
|
one - the base path is compiled into the image. Hence also no URLRewrite
|
||||||
|
filter in Gateway mode.
|
||||||
- No proxy-body-size annotation. Attachment uploads go through the Ingress and
|
- No proxy-body-size annotation. Attachment uploads go through the Ingress and
|
||||||
nginx's default of 1m rejects larger ones with a 413 that reads like an
|
nginx's default of 1m rejects larger ones with a 413 that reads like an
|
||||||
application fault; the right number is the mail server's limit, which this
|
application fault; the right number is the mail server's limit, which this
|
||||||
chart does not know. Set it in ingress.annotations.
|
chart does not know. Set it in ingress.annotations{{ if $httpRoute.enabled }} - except that in GATEWAY
|
||||||
|
MODE there is no annotation to set and no counterpart at all, which is the
|
||||||
|
section above{{ end }}.
|
||||||
|
- No Gateway, no GatewayClass and no listener, in either mode. Those are
|
||||||
|
platform objects and live in cluster-config; this chart renders an
|
||||||
|
HTTPRoute at most, which is the part that looks the same on every Gateway
|
||||||
|
implementation.
|
||||||
@@ -447,17 +447,34 @@ for the first time during a real `helm install`. A guard that only runs in
|
|||||||
production is worse than no guard.
|
production is worse than no guard.
|
||||||
|
|
||||||
The honest position for THIS chart, checked in both directions as guideline 12.1
|
The honest position for THIS chart, checked in both directions as guideline 12.1
|
||||||
asks: nothing outside the Ingress template FUNCTIONALLY reads ingress.domain.
|
asks, AND CORRECTED WITH THE GATEWAY-API SUPPORT (Gitea #114): it used to say
|
||||||
Bulwark derives its own public URL from the request origin and takes no base-URL
|
that nothing outside the Ingress template FUNCTIONALLY reads ingress.domain.
|
||||||
variable, so with ingress.enabled: false the release is complete without a
|
That was true and is no longer - bulwark.httproute.yaml reads it as the route's
|
||||||
domain - the calibre-web-automated case, where the gate frees both values. The
|
hostname, with a required() of its own. What still holds is the half that made
|
||||||
enforcement therefore sits entirely in bulwark.ingress.yaml, where it is reached
|
the claim worth making: Bulwark derives its own public URL from the request
|
||||||
by every render that has the Ingress on (the default), and this helper renders a
|
origin and takes no base-URL variable, so with BOTH modes off the release is
|
||||||
visible placeholder instead of failing.
|
complete without a domain - the calibre-web-automated case, where the gate
|
||||||
|
frees both values. The enforcement sits in whichever routing template is
|
||||||
|
active; this helper still renders a visible placeholder instead of failing,
|
||||||
|
because `helm template` does not render NOTES.txt.
|
||||||
|
|
||||||
|
IN GATEWAY MODE THE PUBLIC URL IS THE ROUTE'S HOSTNAME, not ingress.domain, and
|
||||||
|
the two differ exactly when httpRoute.hostnames is set explicitly - which is
|
||||||
|
how a migration off the Ingress runs. Printing ingress.domain there would hand
|
||||||
|
the operator an OIDC redirect URI for the host being migrated AWAY from, and
|
||||||
|
that failure surfaces as a rejected login at the identity provider rather than
|
||||||
|
as anything about this chart. So the helper follows the active mode.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "bulwark.publicUrl" -}}
|
{{- define "bulwark.publicUrl" -}}
|
||||||
{{- if .Values.ingress.domain -}}
|
{{- $httpRoute := .Values.httpRoute | default dict -}}
|
||||||
https://{{ .Values.ingress.domain }}
|
{{- $host := .Values.ingress.domain -}}
|
||||||
|
{{- if $httpRoute.enabled -}}
|
||||||
|
{{- $host = (first ($httpRoute.hostnames | default list)) | default .Values.ingress.domain -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if $host -}}
|
||||||
|
https://{{ $host }}
|
||||||
|
{{- else if $httpRoute.enabled -}}
|
||||||
|
https://<neither httpRoute.hostnames nor ingress.domain SET>
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
https://<ingress.domain NOT SET>
|
https://<ingress.domain NOT SET>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
{{/*
|
||||||
|
HTTPRoute - the Gateway-API alternative to bulwark.ingress.yaml (Gitea #114).
|
||||||
|
|
||||||
|
ONE ROUTE, ONE RULE, AT THE ROOT - and the reason is the same one that makes
|
||||||
|
the Ingress rule permanently "/": NEXT_PUBLIC_BASE_PATH is compiled into
|
||||||
|
Bulwark's bundle, so the application cannot be served under a sub-path at all.
|
||||||
|
Which is also why THIS CHART RENDERS NO URLRewrite FILTER and needs none. That
|
||||||
|
differs from what #114's measured table and the header of
|
||||||
|
ci/run-httproute-match-test.sh both expected of this chart, so it is worth
|
||||||
|
writing down where it will be read:
|
||||||
|
|
||||||
|
- This chart has ONE Ingress object, not two (keycloak has two).
|
||||||
|
- This chart has NO rewrite-target annotation and CANNOT have one. Every
|
||||||
|
occurrence of the string "rewrite-target" in this chart - values.yaml, the
|
||||||
|
schema, the Ingress template - is an explanation of why it does not help:
|
||||||
|
the bundle's own asset and API URLs are built for the root, so stripping a
|
||||||
|
prefix at the proxy yields a page that loads and then fetches everything
|
||||||
|
from the wrong place. A `git grep rewrite-target bulwark/` finds those
|
||||||
|
refutations and counts them as occurrences, which is how the expectation
|
||||||
|
arose.
|
||||||
|
- Consequently the #118 prefix/redirect assurance in
|
||||||
|
ci/run-httproute-match-test.sh does not apply here: with no URLRewrite on a
|
||||||
|
prefix other than "/", its precondition is never met. The bare-prefix
|
||||||
|
redirect that freesailarr needs has nothing to redirect here.
|
||||||
|
|
||||||
|
WHAT THIS RENDERS, AND WHAT IT MUST NOT
|
||||||
|
|
||||||
|
Only an HTTPRoute 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.
|
||||||
|
|
||||||
|
WHAT THIS MODE DROPS FOR THIS CHART, MEASURED ON THE LIVE OBJECT
|
||||||
|
|
||||||
|
On 2026-10-12 the live Ingress (jk-cluster, namespace bulwark,
|
||||||
|
webmail.bekemeyer-klein.de) carried
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: 25m
|
||||||
|
|
||||||
|
The CHART does not set it - values.yaml recommends exactly that value and
|
||||||
|
explains why the chart cannot pick it (the right number is the mail server's,
|
||||||
|
which this chart does not know), so the 25m is the operator's, supplied through
|
||||||
|
ingress.annotations. It is therefore invisible to anyone reading this
|
||||||
|
repository alone, and it is the single annotation this installation depends on.
|
||||||
|
|
||||||
|
In this mode it is gone. The Gateway API has no body-size counterpart: it is
|
||||||
|
implementation policy, not routing, and decision 2 of #114 says such a limit is
|
||||||
|
DOCUMENTED rather than rebuilt. So the Gateway branch of NOTES.txt names it in
|
||||||
|
this chart's own terms - an attachment above the Gateway's default is rejected
|
||||||
|
before Bulwark sees a byte, with a 413 from the proxy that reads like an
|
||||||
|
application fault, on the one application whose whole purpose is sending files
|
||||||
|
to other people.
|
||||||
|
*/}}
|
||||||
|
{{- /*
|
||||||
|
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 -}}
|
||||||
|
{{- $settings := fromYaml (include "bulwark.settings" .) -}}
|
||||||
|
{{- 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 Service under the same hostname 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 Bulwark it also breaks the login: the OIDC redirect URI registered with the identity provider names ONE origin, and a session that starts on one data path and returns on the other fails on the state cookie. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain) with that second origin registered as a redirect URI too, never over parallel operation of both." -}}
|
||||||
|
{{- 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.
|
||||||
|
|
||||||
|
AND THIS READ CHANGES THE CHART'S OWN ACCOUNT OF ITSELF. Before this mode
|
||||||
|
existed, bulwark.ingress.yaml and the bulwark.publicUrl helper both stated that
|
||||||
|
nothing outside the Ingress template FUNCTIONALLY reads ingress.domain - true
|
||||||
|
then, false now, because this template reads it. Both have been corrected
|
||||||
|
rather than left standing (the same correction garage and calibre-web-automated
|
||||||
|
needed in groups 1 and 2). The required() below is what makes it true again:
|
||||||
|
ingress.domain is mandatory in Gateway mode too, unless hostnames are given
|
||||||
|
explicitly.
|
||||||
|
*/ -}}
|
||||||
|
{{- $hostnames := $httpRoute.hostnames | default list -}}
|
||||||
|
{{- if not $hostnames -}}
|
||||||
|
{{- $hostnames = list (required "A Domain/Host must be provided: set ingress.domain (used as the 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
|
||||||
|
labels:
|
||||||
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
{{- toYaml $parentRefs | nindent 4 }}
|
||||||
|
hostnames:
|
||||||
|
{{- toYaml $hostnames | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
{{- /*
|
||||||
|
The Ingress counterpart is `path: / , pathType: Prefix`, and "/" here is
|
||||||
|
as permanent as it is there - see the header. 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. It comes from
|
||||||
|
the same settings helper the Service renders its port from, so
|
||||||
|
bulwark.port and this file still cannot drift apart;
|
||||||
|
ci/run-httproute-match-test.sh holds the pair for the case where they do.
|
||||||
|
*/}}
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /
|
||||||
|
backendRefs:
|
||||||
|
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||||
|
port: {{ $settings.port | int64 }}
|
||||||
|
{{- end }}
|
||||||
@@ -14,12 +14,22 @@ Prefix, deliberately and permanently.
|
|||||||
|
|
||||||
GUIDELINE 1, CHECKED IN BOTH DIRECTIONS (README, "Values that must not lie"),
|
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:
|
AND FOR THIS CHART THE GATE FREES BOTH VALUES. Checked rather than assumed:
|
||||||
nothing outside this template FUNCTIONALLY reads ingress.domain. Bulwark derives
|
Bulwark derives its own public URL from the request origin and takes no
|
||||||
its own public URL from the request origin and takes no base-URL environment
|
base-URL environment variable - there is no KC_HOSTNAME equivalent here - so a
|
||||||
variable - there is no KC_HOSTNAME equivalent here - so a release with
|
release with no routing object at all is complete without a domain. That is the
|
||||||
ingress.enabled: false is complete without a domain. That is the
|
|
||||||
calibre-web-automated case rather than the matrix-synapse one.
|
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
|
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
|
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
|
template` does not render NOTES.txt, so a guard there would fire for the first
|
||||||
|
|||||||
@@ -166,27 +166,50 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"description": "Gates the whole Ingress template (issue #93). Checked in both directions for this chart: nothing outside that template FUNCTIONALLY reads ingress.domain - Bulwark derives its own public URL from the request origin and takes no base-URL variable - so switching the Ingress off frees domain as well as clusterIssuer. The only other reader is NOTES.txt, which renders a visible placeholder rather than carrying a required() that helm template could never reach.",
|
"description": "Gates the whole Ingress template (issue #93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. Checked in both directions for this chart, and CORRECTED with Gitea #114: it used to say that nothing outside that template functionally reads ingress.domain, which the HTTPRoute now does. Switching the INGRESS off frees clusterIssuer; it frees domain only if httpRoute is off as well. What still holds is the reason - Bulwark derives its own public URL from the request origin and takes no base-URL variable - so a release with no routing object at all is complete without a domain. NOTES.txt renders a visible placeholder rather than carrying a required() that helm template could never reach.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"clusterIssuer": {
|
"clusterIssuer": {
|
||||||
"type": ["string", "null"]
|
"type": ["string", "null"]
|
||||||
},
|
},
|
||||||
"domain": {
|
"domain": {
|
||||||
"description": "The public hostname. Bulwark is served at the ROOT of it and cannot be served under a path: NEXT_PUBLIC_BASE_PATH is compiled into the bundle, and a rewrite-target annotation does not help because the bundle's own asset URLs are built for the root.",
|
"description": "The public hostname. Bulwark is served at the ROOT of it and cannot be served under a path: NEXT_PUBLIC_BASE_PATH is compiled into the bundle, and a rewrite-target annotation does not help because the bundle's own asset URLs are built for the root - which is also why the HTTPRoute renders no URLRewrite filter. Read by the Ingress, by the HTTPRoute (when httpRoute.hostnames is empty) and by NOTES.txt for the OIDC redirect URI.",
|
||||||
"type": ["string", "null"]
|
"type": ["string", "null"]
|
||||||
},
|
},
|
||||||
"className": {
|
"className": {
|
||||||
"type": ["string", "null"]
|
"type": ["string", "null"]
|
||||||
},
|
},
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. The one worth adding for this application is nginx.ingress.kubernetes.io/proxy-body-size: attachment uploads go through this Ingress and nginx's default of 1m rejects anything larger with a 413 that reads like an application fault.",
|
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. The one worth adding for this application is nginx.ingress.kubernetes.io/proxy-body-size: attachment uploads go through this Ingress and nginx's default of 1m rejects anything larger with a 413 that reads like an application fault - and that is what this installation's live Ingress actually carries (25m, measured 2026-10-12). INERT IN GATEWAY MODE: the Gateway API has no annotation channel and no body-size counterpart at all.",
|
||||||
"type": "object"
|
"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 an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. ONE rule, at the root, with no URLRewrite filter - Bulwark cannot be served under a sub-path, so there is nothing to strip and the #118 prefix/redirect assurance has no precondition to meet here. NOT COVERED IN THIS MODE: proxy-body-size, which this installation's live Ingress carries at 25m and which has no counterpart in the specification - an oversized attachment is then rejected by the Gateway with a 413 that reads like a webmail fault. See values.yaml, httpRoute block. 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 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 hostname has a single source of truth. Setting it means a second origin, which has to be registered as an OIDC redirect URI with the identity provider before a login can succeed on it.",
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"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"]
|
||||||
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"type": ["integer", "null"],
|
"type": ["integer", "null"],
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
|
|||||||
@@ -711,6 +711,11 @@ ingress:
|
|||||||
# away would produce a release that renders cleanly and whose operator is told
|
# away would produce a release that renders cleanly and whose operator is told
|
||||||
# to register `https:///auth/callback`.
|
# to register `https:///auth/callback`.
|
||||||
#
|
#
|
||||||
|
# AND A THIRD TIME SINCE Gitea #114: the HTTPRoute reads it as its hostname
|
||||||
|
# when httpRoute.hostnames is empty, with a required() of its own. So this
|
||||||
|
# value is mandatory in GATEWAY mode as well, and only a release with both
|
||||||
|
# modes off can do without it.
|
||||||
|
#
|
||||||
# BULWARK IS SERVED AT THE ROOT OF THIS HOST AND CANNOT BE SERVED UNDER A
|
# BULWARK IS SERVED AT THE ROOT OF THIS HOST AND CANNOT BE SERVED UNDER A
|
||||||
# PATH. NEXT_PUBLIC_BASE_PATH is compiled into the bundle (point 4 at the top
|
# PATH. NEXT_PUBLIC_BASE_PATH is compiled into the bundle (point 4 at the top
|
||||||
# of this file), so a sub-path deployment would need a rebuilt image. The
|
# of this file), so a sub-path deployment would need a rebuilt image. The
|
||||||
@@ -734,4 +739,80 @@ ingress:
|
|||||||
# (or whatever the mail server accepts) to match stalwart's own limit. The
|
# (or whatever the mail server accepts) to match stalwart's own limit. The
|
||||||
# chart does not set it, because the right number is the mail server's and
|
# chart does not set it, because the right number is the mail server's and
|
||||||
# this chart does not know it.
|
# this chart does not know it.
|
||||||
|
#
|
||||||
|
# AND THAT RECOMMENDATION IS NOT HYPOTHETICAL - IT IS WHAT THIS INSTALLATION
|
||||||
|
# RUNS ON. Measured on 2026-10-12: the live Ingress on
|
||||||
|
# webmail.bekemeyer-klein.de carries exactly
|
||||||
|
# nginx.ingress.kubernetes.io/proxy-body-size: 25m, supplied here by the
|
||||||
|
# operator. In GATEWAY MODE this whole block is INERT - the Gateway API has
|
||||||
|
# no annotation channel for controller behaviour, and body size has no
|
||||||
|
# counterpart in the specification at all. Read the httpRoute block below
|
||||||
|
# before switching modes.
|
||||||
annotations: {}
|
annotations: {}
|
||||||
|
|
||||||
|
# THE GATEWAY-API ALTERNATIVE TO THE INGRESS (Gitea #114).
|
||||||
|
#
|
||||||
|
# Mutually exclusive with ingress.enabled - the render aborts when both are
|
||||||
|
# true, and the message names both value paths. The chart renders ONLY an
|
||||||
|
# HTTPRoute with parentRefs: no Gateway, no class, no Certificate, and no
|
||||||
|
# implementation-specific policy object. Class and certificate sit on the
|
||||||
|
# Gateway and its listeners and belong to the platform (cluster-config), which
|
||||||
|
# is what keeps the Gateway implementation replaceable without touching this
|
||||||
|
# chart again.
|
||||||
|
#
|
||||||
|
# ONE ROUTE, ONE RULE, AT THE ROOT. "/" is as permanent here as it is on the
|
||||||
|
# Ingress, for the same reason: NEXT_PUBLIC_BASE_PATH is compiled into the
|
||||||
|
# bundle, so Bulwark cannot be served under a sub-path and there is nothing for
|
||||||
|
# a URLRewrite filter to strip. This chart renders no such filter and needs
|
||||||
|
# none - which also means the #118 prefix/redirect assurance in
|
||||||
|
# ci/run-httproute-match-test.sh has no precondition to meet here.
|
||||||
|
#
|
||||||
|
# WHAT THIS MODE COSTS, AND IT IS THE ONE ANNOTATION THIS INSTALLATION USES.
|
||||||
|
# proxy-body-size: 25m is set on the live Ingress by the operator (see the
|
||||||
|
# annotations block above). It is GONE in this mode and cannot be rebuilt here:
|
||||||
|
# the Gateway API knows no body size, that is implementation policy, and
|
||||||
|
# decision 2 of #114 says the limit is documented rather than reconstructed
|
||||||
|
# with a policy object that would bind this chart to one implementation.
|
||||||
|
#
|
||||||
|
# What that looks like in practice, for THIS application: an attachment above
|
||||||
|
# the GATEWAY's default is rejected before Bulwark sees a byte, and the 413
|
||||||
|
# comes from the proxy. The user is composing mail with a file attached, so the
|
||||||
|
# failure arrives at the least recoverable moment and reads like a webmail
|
||||||
|
# fault. Set the equivalent next to the Gateway - in that implementation's own
|
||||||
|
# object - BEFORE pointing DNS at it, and size it to what the MAIL SERVER
|
||||||
|
# accepts (stalwart), not to what feels generous. NOTES.txt prints this in
|
||||||
|
# Gateway mode.
|
||||||
|
#
|
||||||
|
# THE GATEWAY-API CRDs MUST BE PRESENT. The chart cannot detect them
|
||||||
|
# (.Capabilities.APIVersions is unreliable at template time) and an HTTPRoute
|
||||||
|
# without them fails at install, so this is a value and not a guess.
|
||||||
|
httpRoute:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# The Gateway(s) the route attaches to - a platform object, usually in
|
||||||
|
# another namespace, whose listener must allow this namespace in
|
||||||
|
# allowedRoutes. Entries are the Gateway API's ParentReference (name, and
|
||||||
|
# optionally namespace, sectionName, port, group, kind), passed through
|
||||||
|
# unchanged:
|
||||||
|
#
|
||||||
|
# parentRefs:
|
||||||
|
# - name: public
|
||||||
|
# namespace: gateway
|
||||||
|
# sectionName: https
|
||||||
|
#
|
||||||
|
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
|
||||||
|
# list, because a route without a parent is created, is valid and routes
|
||||||
|
# nothing, with no event to say so.
|
||||||
|
parentRefs: []
|
||||||
|
|
||||||
|
# Empty means [ingress.domain], so the hostname has one source of truth. Set
|
||||||
|
# explicitly only to publish under a different or an additional name - which
|
||||||
|
# is how a migration from the Ingress runs: the HTTPRoute takes a second
|
||||||
|
# hostname while the Ingress keeps ingress.domain.
|
||||||
|
#
|
||||||
|
# AND IF YOU SET IT, REGISTER THAT ORIGIN WITH THE IDENTITY PROVIDER FIRST.
|
||||||
|
# NOTES.txt derives the OIDC redirect URI from the ACTIVE mode's hostname, so
|
||||||
|
# a second hostname means a second redirect URI - a login that returns to an
|
||||||
|
# unregistered origin is rejected at the provider, which says nothing about
|
||||||
|
# this chart.
|
||||||
|
hostnames: []
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Render-only overlay for ci/run-httproute-match-test.sh, applied LAST.
|
||||||
|
#
|
||||||
|
# Its one job is to put this chart into Gateway mode, because the convention
|
||||||
|
# ships with httpRoute.enabled: false and the test would otherwise be green
|
||||||
|
# over nothing. A chart that has an overlay here and still renders no
|
||||||
|
# HTTPRoute FAILS the test - that is the whole point of the file existing.
|
||||||
|
#
|
||||||
|
# An overlay and not a change to ci/bulwark/test-values.yaml: the install-test
|
||||||
|
# runs exactly ONE configuration per chart, and this chart's fixture installs
|
||||||
|
# the StatefulSet with its PVC and both 1Password items. Trading its Ingress
|
||||||
|
# for a CRD schema check that cyberchef's fixture already performs against the
|
||||||
|
# real Gateway-API CRDs would be a bad swap. The install keeps the Ingress; the
|
||||||
|
# route is held here, at render level, which is where its silent failure modes
|
||||||
|
# live.
|
||||||
|
#
|
||||||
|
# ingress.enabled: false is not optional here - ingress.enabled is this chart's
|
||||||
|
# routing switch and defaults to TRUE, so without this line the chart aborts
|
||||||
|
# its own render with both modes on.
|
||||||
|
#
|
||||||
|
# ONE ROUTING OBJECT, WHICH IS ALL THIS CHART HAS. #114's measured table and
|
||||||
|
# the header of ci/run-httproute-match-test.sh both expected two Ingresses and
|
||||||
|
# a rewrite-target here; neither is the case, and the reason is in
|
||||||
|
# bulwark.httproute.yaml. In short: Bulwark cannot be served under a sub-path
|
||||||
|
# (NEXT_PUBLIC_BASE_PATH is compiled into the bundle), so there is one object,
|
||||||
|
# one rule, at "/", and no URLRewrite filter for the #118 prefix/redirect
|
||||||
|
# assurance to pair up with. Every "rewrite-target" string in this chart is an
|
||||||
|
# explanation of why it does not work, and a grep counted those as
|
||||||
|
# occurrences.
|
||||||
|
#
|
||||||
|
# WHAT THIS OVERLAY COVERS:
|
||||||
|
#
|
||||||
|
# - ingress.domain comes from the fixture, so the route renders with its
|
||||||
|
# hostname from that single source of truth - httpRoute.hostnames stays
|
||||||
|
# empty on purpose, because the fallback is the path every installation
|
||||||
|
# will actually use.
|
||||||
|
# - the route's backendRef is checked against the serving Service of the same
|
||||||
|
# render, by PORT NUMBER (bulwark.port) rather than by the name "http" the
|
||||||
|
# Ingress uses, because backendRefs takes a PortNumber. This overlay is
|
||||||
|
# what holds that number against the Service's.
|
||||||
|
#
|
||||||
|
# NOT covered here, and it cannot be: that the hostname answers. No Gateway
|
||||||
|
# controller runs in CI and no route reaches "Accepted: True". Nor the thing
|
||||||
|
# this chart actually loses in this mode - the operator's
|
||||||
|
# proxy-body-size: 25m, which has no counterpart in the specification. That
|
||||||
|
# loss is documented in values.yaml and printed by NOTES.txt, and documentation
|
||||||
|
# is not something a test can assert.
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
httpRoute:
|
||||||
|
enabled: true
|
||||||
|
parentRefs:
|
||||||
|
- name: public
|
||||||
|
namespace: gateway
|
||||||
|
sectionName: https
|
||||||
@@ -43,9 +43,18 @@
|
|||||||
# property of one route - a different route is a different hostname and
|
# property of one route - a different route is a different hostname and
|
||||||
# says nothing about this one. Exact outranks every prefix match in that
|
# says nothing about this one. Exact outranks every prefix match in that
|
||||||
# precedence, so the pairing is about existence and not about order.
|
# precedence, so the pairing is about existence and not about order.
|
||||||
# freesailarr is the first chart this applies to; bulwark is the second
|
# freesailarr is the FIRST AND, AS OF GROUP 3 OF #114, THE ONLY chart this
|
||||||
# rewrite-target chart and meets the same check when group 3 of #114
|
# applies to. The line that used to stand here said bulwark was the second
|
||||||
# lands, which is the point of checking it here rather than in one chart.
|
# rewrite-target chart and would meet the same check when group 3 landed.
|
||||||
|
# Group 3 landed and it does not: bulwark has ONE Ingress object, no
|
||||||
|
# rewrite-target annotation, and cannot have one - NEXT_PUBLIC_BASE_PATH is
|
||||||
|
# compiled into its bundle, so it cannot be served under a sub-path at all
|
||||||
|
# and its route is one rule at "/" with no URLRewrite filter. Every
|
||||||
|
# occurrence of the string "rewrite-target" in that chart is an explanation
|
||||||
|
# of why it does not help, and #114's measured table arrived at "bulwark,
|
||||||
|
# freesailarr" by grepping for the string rather than for the annotation.
|
||||||
|
# The check stays over all charts for the reason below - the mistake
|
||||||
|
# arrives by copy - not because a second chart is known to need it.
|
||||||
#
|
#
|
||||||
# And the install-test cannot close either gap, even with the real CRDs in the
|
# And the install-test cannot close either gap, even with the real CRDs in the
|
||||||
# cluster: schema validation accepts an empty parentRefs list and accepts a
|
# cluster: schema validation accepts an empty parentRefs list and accepts a
|
||||||
|
|||||||
Reference in new issue
Block a user