Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 16s
Validate Helm Charts / Selector match (pull_request) Successful in 15s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 16s
Validate Helm Charts / Build stalwart (pull_request) Successful in 9s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 29s
Install-test / Install-test stalwart (pull_request) Successful in 1m42s
Validate Helm Charts / Install-test (pull_request) Successful in 1m42s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 13s
Publish Stalwart Helm Chart / Publish Stalwart Helm Chart (push) Successful in 13s
SonarQube Analysis / SonarQube Analysis (push) Successful in 50s
Maintainer's decision, and it reverses this chart's naming rather than the
style guide's rule. The objects become
<release>-stalwart-mta-sts-ingress, -1, -2, ...
in this repo's idiom ({{ if $i }}-{{ $i }}{{ end }}), the same as
homeassistant's routes and keycloak's realm Ingresses, inlined in the
template instead of behind a helper of its own.
WHY IT GOES THIS WAY. This chart named by issuer, reading the style guide's
index rule as resting on "a route holds no state and no certificate" and
observing that these objects hold one. That reading was right and the
rule's STATED REASON was wrong: the parallel #122 round named by index,
cited the same rule as binding, and entered keycloak into it - whose realm
Ingresses hold certificates too. Two charts did opposite things for one
shape, and the rule had been extended onto cases its own precondition
excluded. #125 is merged and published, so the reason is repaired and the
naming unified on index; keycloak is not touched.
What actually carries the index rule, in three steps that have different
standing and are written as such in the template header and the style guide:
cert-manager here runs WITHOUT --enable-certificate-owner-ref (MEASURED by
the maintainer - absent from the Deployment's args and from the
ControllerConfiguration its --config names, which carries only
kind: ControllerConfiguration and enableGatewayAPI: true); by that default
the Certificate is collected with its Ingress but the SECRET IS NOT
(DOCUMENTED behaviour, not measured here), so a rename recreates the
Certificate object against a surviving secret and issues nothing; and NO
RENAME WAS APPLIED against a cluster. The promise therefore hangs on a
cluster-level flag this repository does not control - with it on, a reorder
would cost a fresh ACME request per renumbered object.
UNCHANGED: the structure. One Ingress per distinct clusterIssuer, one rule
per host, tls grouped by secret, and the abort when one secret spans two
issuers - grouping is a cert-manager question and naming is a style
question, and they are independent.
TWO THINGS THE RENAME OBLIGES.
1. NOTES.txt now prints the mapping - index, object name, issuer, secrets
and hosts per object, the way keycloak's does for its realms - because
the name no longer carries the issuer and nothing else in the cluster
says which object holds which.
2. The reordering hazard is documented as SHARPER than homeassistant's:
there an index names a host, here it names an issuer GROUP derived from
the list, so moving one entry can change the grouping and renumber
objects that were not themselves moved.
AN ASYMMETRY WITH keycloak, named in the template header: there both modes
render one object per entry, so the index question arises twice. Here it
arises on the Ingress side ONLY - Gateway mode renders one HTTPRoute
carrying every hostname, because TLS is none of the route's business, so
there is no index and no name to go stale.
Version stays 1.3.0+upv0.16.25: nothing is published yet, so nothing is
being renamed in a release.
Re-measured after the rename, not assumed: three empty helm template diffs
against origin/main (defaults, CI fixture, overlay), helm lint --strict
clean three ways, every abort path still firing with the structural one
still winning over the per-entry one, the schema still rejecting
mtaSts.enabled, and the HTTPRoute render test still at 64 assertions over
22 charts - object names are not assertions.
220 lines
11 KiB
YAML
220 lines
11 KiB
YAML
{{- /*
|
|
The Ingress for the WebUI and JMAP only. SMTP, IMAP and ManageSieve are not
|
|
HTTP and cannot pass through it - those reach the pods through the per-port
|
|
NodePort Services.
|
|
|
|
It targets exactly ONE node group (ingress.nodeGroup) through that group's own
|
|
http Service, never a port-label selector: the management surface is not safe
|
|
to spread across a group that may not run the management tasks.
|
|
|
|
TWO TRAPS THAT BELONG TO THIS OBJECT AND ARE NOT SOLVED BY IT:
|
|
|
|
1. The auto-ban sees the INGRESS CONTROLLER's pod IP for every JMAP and WebUI
|
|
request, not the client's. Sooner or later it bans that address and the
|
|
WebUI and JMAP go dark for everyone at once. The PROXY protocol is not the
|
|
remedy for HTTP: the remedy is Stalwart's "forwarded" handling with
|
|
X-Forwarded-For evaluation, plus discarding whatever forwarding headers the
|
|
client brought along. That is a configuration step in the database, and it
|
|
is mandatory rather than optional - the failure arrives weeks later and
|
|
looks like a server problem.
|
|
|
|
2. AFTER BOOTSTRAP, PORT 8080 IS NOT THE ADMIN PORT ANY MORE. Upstream:
|
|
"After the restart, the WebUI is reached at https://<hostname>/admin on the
|
|
configured server hostname, and HTTP access on port 8080 is no longer
|
|
available for administration." So this Ingress only works once an HTTP
|
|
NetworkListener on 8080 exists in the database. Between finishing the setup
|
|
and creating that listener, the Ingress resolves to a port that answers
|
|
nothing.
|
|
*/ -}}
|
|
{{- if .Values.ingress.enabled }}
|
|
{{- $group := include "stalwart.ingressNodeGroup" . -}}
|
|
{{- $nodes := fromYaml (include "stalwart.nodes" .) -}}
|
|
{{- $spec := index $nodes $group -}}
|
|
{{- if not (has "http" $spec.exposedPorts) -}}
|
|
{{- fail (printf "ingress.nodeGroup is %q, but nodes.%s.exposedPorts does not include \"http\" - there would be no Service for the Ingress to point at" $group $group) -}}
|
|
{{- end -}}
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: {{ include "stalwart.fullname" . }}-ingress
|
|
labels:
|
|
app: {{ include "stalwart.fullname" . }}
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
|
|
{{- 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:
|
|
# The http Service of the "{{ $group }}" group only.
|
|
name: {{ include "stalwart.fullname" . }}-{{ $group }}-http-service
|
|
port:
|
|
name: http
|
|
path: /
|
|
pathType: Prefix
|
|
tls:
|
|
- hosts:
|
|
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
|
secretName: tls-{{ include "stalwart.fullname" . }}-ingress
|
|
{{- end }}
|
|
{{- /*
|
|
MTA-STS (Gitea #123) - the policy route, in Ingress mode.
|
|
|
|
ONE OBJECT PER clusterIssuer, one rule per host, tls grouped by secret. The
|
|
whole reasoning for that shape, including the cert-manager measurement it rests
|
|
on, is in stalwart.mtaSts.ingressGroups in _helpers.tpl; the short version is
|
|
that the issuer is an object-level ANNOTATION while the secret is a per-tls-entry
|
|
field, so an object cannot both ask cert-manager for one certificate and leave
|
|
another alone.
|
|
|
|
GATED ON ingress.enabled, not on a switch of its own. mtaSts.hosts is the
|
|
switch for the feature (empty list = nothing rendered, in either mode) and
|
|
ingress.enabled is the switch for the MODE - so these objects appear exactly
|
|
when this chart is publishing over an Ingress AND the list is non-empty. The
|
|
mutual exclusion of the two modes is enforced once, in stalwart.httproute.yaml,
|
|
and it is unconditional on mtaSts: a fail anywhere aborts the whole render.
|
|
|
|
AND "not httpRoute.enabled" IS PART OF THAT GATE RATHER THAN REDUNDANT WITH
|
|
IT. Without it this block's per-entry required() can fire while BOTH modes are
|
|
on, and then the render reports "mtaSts.hosts[0] has no tlsSecretName, and in
|
|
Ingress mode every entry needs one" - true, and the wrong diagnosis: the
|
|
values file's defect is that both modes are on, and "in Ingress mode" is a
|
|
claim about a configuration that is not coherent at all. Whoever met it would
|
|
add a tlsSecretName, re-run, and reach the real message one round later, or
|
|
conclude this feature was the problem. MEASURED on this chart before the gate
|
|
was added, not reasoned about.
|
|
|
|
Ordering is not the fix and could not be: Helm renders templates in an order a
|
|
chart cannot pin, so this template may not assume the httproute template's
|
|
structural abort runs first. SCOPE is the fix - with both modes on this block
|
|
validates nothing, so the only fail left is the one that names the real
|
|
problem. The generalisation (a specific validation must not be reachable in a
|
|
state a structural validation already rejects) is the Lessons-Learned entry
|
|
that the parallel keycloak round of Gitea #122 bought on its own realm list.
|
|
|
|
WHAT THIS DELIBERATELY DOES NOT CARRY: ingress.annotations. Those belong to the
|
|
WebUI/JMAP host - on the live installation they are its CORS configuration for
|
|
the bulwark webmail - and a policy file served to sending MTAs has no use for
|
|
them. Copying them here would put a CORS allow-origin on an endpoint no browser
|
|
ever calls, and would do it silently.
|
|
|
|
AND THE LIMIT THAT NO OBJECT HERE CAN FIX: this chart registers a ROUTE, it
|
|
does not create a DOMAIN. Whether Stalwart answers for mta-sts.<domain> depends
|
|
on <domain> existing in its data store, which is not in any chart value (see
|
|
values.yaml, line 1 onwards, and the mtaSts block).
|
|
|
|
THE NAMES ARE INDEXED OVER THE ISSUER GROUPS, AND THE INDEX IS ONLY AN INGRESS
|
|
QUESTION
|
|
|
|
"<release>-stalwart-mta-sts-ingress", then "-1", "-2" - the repo's idiom
|
|
({{ if $i }}-{{ $i }}{{ end }}), as homeassistant's routes and keycloak's realm
|
|
Ingresses use it (style guide section 4). The index runs over the ISSUER
|
|
GROUPS, not over mtaSts.hosts entries: the grouping is a separate decision,
|
|
made for cert-manager's sake and argued in stalwart.mtaSts.ingressGroups, and
|
|
it is untouched by how the objects are named.
|
|
|
|
AN ASYMMETRY WITH keycloak WORTH KNOWING: there, both modes render one object
|
|
per entry, so the index question arises twice. HERE IT ARISES ONLY ON THIS
|
|
SIDE. Gateway mode renders exactly ONE HTTPRoute carrying every hostname,
|
|
because TLS is not involved there at all - no certificate, no secret, nothing
|
|
to split an object over - so there is no index and no name to go stale. The
|
|
whole of the naming question below is an Ingress-mode question.
|
|
|
|
THE REORDERING HAZARD IS SHARPER HERE THAN AT homeassistant, so read this
|
|
before shuffling mtaSts.hosts. There, an index names a host, and reordering
|
|
changes which index serves which host. Here an index names an ISSUER GROUP
|
|
derived from the list, so reordering can change THE GROUPING ITSELF - how many
|
|
objects exist, and which index carries which issuer and which hosts. Moving
|
|
one entry can therefore renumber objects that were not themselves moved. What
|
|
makes it bearable rather than a reason to avoid indices:
|
|
|
|
1. MEASURED (by the maintainer, in jk-cluster, on this round): cert-manager
|
|
runs WITHOUT --enable-certificate-owner-ref - the flag appears neither in
|
|
the cert-manager Deployment's args nor in the ControllerConfiguration its
|
|
--config points at (which carries only kind: ControllerConfiguration and
|
|
enableGatewayAPI: true). So the controller's default applies.
|
|
2. DOCUMENTED BEHAVIOUR of that default, not measured here: the Certificate
|
|
is garbage-collected with the Ingress it is owner-referenced from, but the
|
|
SECRET is not - cert-manager only adds an owner reference to the Secret
|
|
when that flag is on. A rename therefore deletes and recreates the
|
|
Certificate object while the Secret stays, so the new Certificate finds a
|
|
valid secret already present and ISSUES NOTHING. No ACME request, no rate
|
|
limit touched.
|
|
3. NOT APPLIED: no rename was actually performed against a cluster. The two
|
|
steps above are the mechanism, not a trial of it.
|
|
|
|
And note what the promise hangs on: a CLUSTER-LEVEL FLAG this repository does
|
|
not control. With --enable-certificate-owner-ref switched on, the Secret would
|
|
be collected with the Certificate and a reorder WOULD mean a re-issue for every
|
|
renumbered object. That is worth checking before anyone turns it on, and it is
|
|
not something a chart can defend itself against.
|
|
|
|
Because the name no longer carries the issuer, NOTES.txt prints the mapping -
|
|
index, issuer and hosts per object - the way keycloak's does for its realms.
|
|
Without it, nothing in the cluster says which object holds which issuer.
|
|
*/ -}}
|
|
{{- $httpRoute := .Values.httpRoute | default dict }}
|
|
{{- if and .Values.ingress.enabled (not $httpRoute.enabled) }}
|
|
{{- $mtaSts := fromYaml (include "stalwart.mtaSts.ingressGroups" .) }}
|
|
{{- if $mtaSts.order }}
|
|
{{- $group := include "stalwart.ingressNodeGroup" . }}
|
|
{{- $nodes := fromYaml (include "stalwart.nodes" .) }}
|
|
{{- $spec := index $nodes $group }}
|
|
{{- if not (has "http" $spec.exposedPorts) }}
|
|
{{- fail (printf "ingress.nodeGroup is %q, but nodes.%s.exposedPorts does not include \"http\" - there would be no Service for the MTA-STS Ingress to point at" $group $group) }}
|
|
{{- end }}
|
|
{{- $service := printf "%s-%s-http-service" (include "stalwart.fullname" .) $group }}
|
|
{{- $path := include "stalwart.mtaSts.path" . }}
|
|
{{- range $i, $issuer := $mtaSts.order }}
|
|
{{- $entry := index $mtaSts.groups $issuer }}
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: "{{ include "stalwart.fullname" $ }}-mta-sts-ingress{{ if $i }}-{{ $i }}{{ end }}"
|
|
labels:
|
|
app: {{ include "stalwart.fullname" $ }}
|
|
{{- if $issuer }}
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: {{ $issuer }}
|
|
{{- end }}
|
|
spec:
|
|
ingressClassName: {{ $.Values.ingress.className }}
|
|
rules:
|
|
{{- range $host := $entry.hosts }}
|
|
- host: {{ $host }}
|
|
http:
|
|
paths:
|
|
{{- /*
|
|
Exact, not Prefix, and the same per-group http Service the WebUI
|
|
rule above names - by PORT NAME, which an Ingress backend can do
|
|
and a Gateway backendRef cannot. RFC 8461 serves exactly one file
|
|
at exactly one path; a Prefix here would hand this host's whole
|
|
URL space to the mail server's management surface.
|
|
*/}}
|
|
- backend:
|
|
service:
|
|
name: {{ $service }}
|
|
port:
|
|
name: http
|
|
path: {{ $path }}
|
|
pathType: Exact
|
|
{{- end }}
|
|
tls:
|
|
{{- range $secret := $entry.tlsOrder }}
|
|
- hosts:
|
|
{{- range $host := index $entry.tls $secret }}
|
|
- {{ $host }}
|
|
{{- end }}
|
|
secretName: {{ $secret }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|