Style guide: repair the index-naming rule's reason, and align stalwart on index
The rule read "acceptable only because a route holds no state and no certificate". That was true of homeassistant, the chart it was written for, and false of every chart it was later applied to: keycloak's realm Ingresses (#122) and stalwart's MTA-STS Ingresses (#123) both own cert-manager Certificates, so the rule's own precondition excluded the cases it was being cited for. It surfaced only because the two rounds ran in parallel and answered the same sentence differently - keycloak by index citing the rule, stalwart by issuer citing the rule's precondition. The maintainer settled it on index (keycloak was already published) and the REASON is what gets corrected here, not the naming of two charts. The reason that holds, in three steps kept apart because their standing differs: cert-manager here runs without --enable-certificate-owner-ref (MEASURED - absent from the Deployment args and from the ControllerConfiguration, which carries only kind and enableGatewayAPI); by that default the Certificate is collected with its Ingress but the SECRET IS NOT (DOCUMENTED behaviour); and no rename was ever performed (NOT APPLIED). So a rename recreates the Certificate against a surviving secret and issues nothing - and the promise hangs on a cluster flag this repository does not control. Also: stalwart added to that rule's examples; the per-entry-issuer paragraph corrected from "name after the issuer" to "name by index" with the obligation that follows (NOTES.txt must print index -> issuer -> hosts, since the name no longer carries it); the sharper reorder hazard recorded (the index runs over groups derived from the list, so a reorder can renumber objects that did not move); and the note that the index question only arises where a mode renders one object per entry - stalwart's Gateway mode renders a single route, so it has the question on the Ingress side only. homeassistant stays named as the certificate-free original case. Lessons learned: my own point 3 said the opposite and is replaced by what the round actually taught - a rule can be right while its written reason is wrong, a justification is load-bearing because it is what someone reaches for when the rule meets an unforeseen case, and parallel rounds are a conformance test nobody wrote (sequentially the second would have followed the first and the broken reason would have survived). stalwart: one half-sentence that was missing from "TLS belongs to the platform" - cert-manager runs here with enableGatewayAPI: true, so the listener certificates can be issued by it from an annotation on the Gateway. The work moves to the Gateway object rather than disappearing.
3 files changed
+24
-4
No files matched your search
+12
-2
@@ -48,7 +48,7 @@ Annotations are per entry too (the set is not uniform across the hostnames — t
|
||||
|
||||
**Documented exception — freesailarr** has no `ingress.domain` at all and offers both shapes — and therefore no `httpRoute.hostnames` either, the only chart without that key; its hostnames stay in `ingress.host` and `<app>.ingress.host`. See [its page](freesailarr).
|
||||
|
||||
**A list of hosts each carrying its own TLS secret *and* its own optional issuer splits into several Ingress objects, and the split is on the issuer.** Added with Gitea #123 (`stalwart.mtaSts.hosts`), and it is a rule rather than a preference because cert-manager reads the two fields at different scopes: `cert-manager.io/cluster-issuer` is an **annotation**, so object-scoped and absent-means-nothing-issued-for-this-object, while `secretName` is **`tls`-entry-scoped** and yields one Certificate per entry. One object therefore cannot both request a certificate from issuer X and leave another host's secret alone — which is what an empty per-entry issuer has to mean, for the reason already in the shared-domain rule above (exactly one certificate per secret). So: **group by issuer for the objects, by secret for the `tls` list** (the `rss-bridge.tlsGroups` helper is the shape to reuse), and **abort the render when one secret appears under two different issuers** — ingress-shim names the Certificate after the secret and owns it from the Ingress, so that case is one Certificate name with two owners. Name such objects after the **issuer**, never after the entry's index: an index is only free where the object holds no certificate. Receipt: [Lessons learned](Lessons-Learned#the-two-fields-cert-manager-reads-at-different-scopes).
|
||||
**A list of hosts each carrying its own TLS secret *and* its own optional issuer splits into several Ingress objects, and the split is on the issuer.** Added with Gitea #123 (`stalwart.mtaSts.hosts`), and it is a rule rather than a preference because cert-manager reads the two fields at different scopes: `cert-manager.io/cluster-issuer` is an **annotation**, so object-scoped and absent-means-nothing-issued-for-this-object, while `secretName` is **`tls`-entry-scoped** and yields one Certificate per entry. One object therefore cannot both request a certificate from issuer X and leave another host's secret alone — which is what an empty per-entry issuer has to mean, for the reason already in the shared-domain rule above (exactly one certificate per secret). So: **group by issuer for the objects, by secret for the `tls` list** (the `rss-bridge.tlsGroups` helper is the shape to reuse), and **abort the render when one secret appears under two different issuers** — ingress-shim names the Certificate after the secret and owns it from the Ingress, so that case is one Certificate name with two owners. **Name such objects by index all the same**, per the index rule above and its corrected reason — the grouping decision and the naming decision are independent, and an earlier draft of `stalwart` that named them by issuer was brought back into line. Note what the index then runs over: the **groups**, not the list, so a reorder can change the grouping and renumber objects that did not move. `NOTES.txt` must therefore print the index → issuer → hosts mapping, because the name no longer carries the issuer and nothing else in the cluster says which object holds which. Receipt: [Lessons learned](Lessons-Learned#the-two-fields-cert-manager-reads-at-different-scopes).
|
||||
|
||||
**And a feature that spans both routing modes must render in both.** A values block that produced objects only in Gateway mode (or only in Ingress mode) would make a mode switch silently drop a feature — the same class as the annotation losses of the sweep, but self-inflicted. Where one mode genuinely cannot carry part of it, that part is **inert and documented, not required**: `stalwart`'s `mtaSts.hosts[].tlsSecretName` is required in Ingress mode and ignored in Gateway mode, where TLS belongs to the listener, so neither mode demands a value that does nothing ([§12](#12-values-that-must-not-lie)).
|
||||
|
||||
@@ -74,10 +74,20 @@ The rest of the rules, each against a failure that reports nothing:
|
||||
| **`parentRefs` mandatory in Gateway mode**, render abort on the empty list | a route without a parent attaches to nothing: created, schema-valid, rejected by nobody, routing nothing — no event, no condition, the only symptom a hostname that does not answer. |
|
||||
| **One source of truth for the hostname** — empty `hostnames` means `[ingress.domain]` | two values that have to agree drift apart. `ingress.domain` is read in Gateway mode too: it names the release's public hostname and is not an Ingress switch. A chart without an `ingress.domain` **omits `hostnames` entirely** and keeps its existing host values — and its schema rejects the key, so the absence is enforced rather than merely intended. Two such charts: `freesailarr` (`ingress.host` / `<app>.ingress.host`) and `homeassistant` (`ingress.hosts`, the flexible list). |
|
||||
| **Several routing objects where the Ingress had several hosts, or several rules with different backends** | `HTTPRoute.spec.hostnames` applies to the **whole object**, so one route can carry several hostnames only when they want the *same* rules. A chart whose hosts want different paths or different backends renders **one object per host**: `keycloak` (public and admin hostname), `matrix-synapse` (homeserver at `/`, delegation nginx at `/.well-known/matrix`), `homeassistant` (one per `ingress.hosts` entry). Where the hostnames differ in *kind* rather than merely in value, the per-object parent gets its own value falling back to the chart-global one — `httpRoute.admin.parentRefs`, `httpRoute.delegation.parentRefs`. That fallback is the counterpart of `ingress.<x>.className` falling back to `ingress.className`, because **"which data path" is a property of the Gateway in this API, not of the route**. |
|
||||
| **Index-based object names where the count is data-driven** | a hostname is not a dependable object-name fragment (the 63-character label limit plus the release prefix; a sanitised hostname can collide). `homeassistant` names its routes `…-httproute`, `…-httproute-1`, … by list index, and `keycloak`'s realm objects follow it in both modes (`…-realm-ingress-1`, `…-realm-httproute-1`) — with the consequence written down rather than discovered: **reordering the list renames routes**, so they are recreated rather than edited. Acceptable only because a route holds no state and no certificate. |
|
||||
| **Index-based object names where the count is data-driven** | a hostname is not a dependable object-name fragment (the 63-character label limit plus the release prefix; a sanitised hostname can collide). `homeassistant` names its routes `…-httproute`, `…-httproute-1`, … by list index; `keycloak`'s realm objects follow it in both modes (`…-realm-ingress-1`, `…-realm-httproute-1`), and `stalwart`'s MTA-STS Ingresses do on the Ingress side (`…-mta-sts-ingress-1`). Consequence written down rather than discovered: **reordering the list renames objects**, so they are recreated rather than edited. **What makes that acceptable is that the Secret survives — not that no certificate is involved.** See the note below. |
|
||||
| **A chart that still renders its Ingress unconditionally gets an `ingress.enabled: true` first** | without a second value path there is nothing for the mutual-exclusion abort to name, and the Ingress plus its unconditional `required()` on `clusterIssuer` would keep rendering in Gateway mode — a value demanded for nothing. `paperless-ngx` was the last such chart and got the switch with group 2; the default keeps the render identical. |
|
||||
| **`NOTES.txt` names the active mode** and what it costs, as `keycloak` already does for its cases | a mode nobody can see from the outside |
|
||||
|
||||
**The index rule's reason, corrected — the first version of it was true of the first chart and not of the rule.** It used to read *"acceptable only because a route holds no state and no certificate"*, and that justification died the moment the rule was extended past routes. `keycloak`'s realm **Ingresses** (Gitea #122) and `stalwart`'s MTA-STS **Ingresses** (Gitea #123) both own cert-manager Certificates, so the stated precondition excluded the very cases the rule was being applied to. Caught because the two rounds ran in parallel and named their objects *differently* for the same shape — one by index citing this rule, one by issuer citing this rule's precondition. The naming was unified on **index** and the reason repaired; the naming of the two merged charts was not disturbed.
|
||||
|
||||
What actually carries it, in three steps that are worth keeping apart because they have different standing:
|
||||
|
||||
1. **Measured** in `jk-cluster`: cert-manager runs **without `--enable-certificate-owner-ref`** — the flag is in neither the `cert-manager` Deployment's args nor the `ControllerConfiguration` its `--config` points at (that file carries only `kind: ControllerConfiguration` and `enableGatewayAPI: true`). The controller's default therefore applies.
|
||||
2. **Documented behaviour** of that default, not measured here: the Certificate is owner-referenced from the Ingress and is garbage-collected with it, but cert-manager adds an owner reference to the **Secret** only when that flag is on. So a rename recreates the *Certificate object* while the **Secret stays**, the new Certificate finds a valid secret already present, and **nothing is issued** — no ACME request, no rate limit touched.
|
||||
3. **Not applied:** no rename was performed against a cluster on either round. Steps 1–2 are the mechanism, not a trial of it.
|
||||
|
||||
**And 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 reordering a list *would* cost a fresh issuance per renamed object. Worth checking before anyone enables it; a chart cannot defend itself against it. Two further notes for whoever applies this rule next: the index may run over something **derived** from the list rather than over the list itself — `stalwart` indexes *issuer groups*, so a reorder can change the grouping and renumber objects that did not move — and the index question only arises where a mode renders **one object per entry**. `stalwart` has it on the Ingress side only, because its Gateway mode renders a single route carrying every hostname, TLS being none of the route's business. `homeassistant` remains the certificate-free original case.
|
||||
|
||||
**What has no counterpart, and must be said rather than rebuilt.** The Gateway API has no annotation channel for controller behaviour. `rewrite-target` has a real equivalent (the `URLRewrite` filter in a rule); **`proxy-body-size` has none** — an upload limit is implementation-specific policy. Every chart that sets or recommends one must print in its Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway, because whoever overlooks that silently inherits the Gateway's default and meets it as a *proxy* error instead of an application error: `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline` since group 2, `bulwark` and `matrix-synapse` since group 3 — each in the chart's own terms rather than in one copied sentence. The same holds for auth annotations in front of an app with no login of its own: in Gateway mode that escape hatch does not exist in the chart at all — `cyberchef` is the live example.
|
||||
|
||||
**And the screen is "needs one", not "sets one".** `garage` sets `annotations: {}` and still depends on an upload limit, because ingress-nginx truncates S3 uploads at 1 MB; `freesailarr` sets none and needs none itself, but `ingress.annotations` is the only lever its operator has for one. Both say so in their Gateway-mode notes. Read values comments and `NOTES.txt`, not only templates.
|
||||
|
||||
+5
-1
@@ -771,6 +771,10 @@ Two Ingress objects whose `tls` entries name the same secret therefore drive the
|
||||
|
||||
1. **The failure this avoids is not a render error.** Every shape above produces valid YAML that a cluster accepts. What goes wrong is one Certificate with two claimants, or a certificate silently requested for a secret another release owns — visible in cert-manager's log and nowhere in the release.
|
||||
2. **"One object" is a shape, not a requirement.** Where an approved plan names an object count, check it against the scope at which the controller reads each field before implementing it. Writing the contradiction down beat resolving it quietly in either direction.
|
||||
3. **An index is not a free object name once a certificate hangs off it.** [homeassistant](homeassistant) names its routes by index and records that reordering recreates them — *"harmless (a route holds no state or certificate)"*. That parenthesis is a precondition, not an aside: an Ingress that owns a cert-manager Certificate fails it, so these objects are named by their issuer instead.
|
||||
3. **A rule can be right while its written reason is wrong — and the reason is what the next person reads.** The index-naming rule said *"acceptable only because a route holds no state and no certificate"*. Reading that literally, this round named its objects by **issuer** instead, since an Ingress that owns a cert-manager Certificate fails that precondition. The parallel realm-host round read the same rule and named by **index**, citing it as binding — and added `keycloak` to it, whose realm **Ingresses** also own certificates. Two charts, one shape, opposite answers, and a rule extended onto cases its own stated precondition excluded.
|
||||
|
||||
**It was the reason that was broken, not the rule.** The maintainer settled it on **index** — `keycloak` was already merged and published, so unifying the other way would have meant renaming a shipped chart for a sentence — and the justification was replaced with the one that holds: **the Secret survives a rename**, so only the Certificate *object* is recreated and nothing is re-issued. Measured for this cluster (`--enable-certificate-owner-ref` is not set, so cert-manager's default applies and it owner-references the Certificate but not the Secret); documented behaviour for the consequence; **not applied** — no rename was performed. And the promise rests on a **cluster flag this repository does not control**: turn that flag on and a reorder costs a fresh ACME request per renamed object.
|
||||
|
||||
Three transferable pieces. **A justification carried in a rule is load-bearing** — it is what someone reaches for when the rule meets a case its author did not have, so a reason that is merely true of the first instance will mislead at the second. **Parallel rounds are a conformance test nobody wrote**: this surfaced only because two agents implemented the same shape from the same sentence at the same time and disagreed; sequentially, the second would simply have followed the first and the broken reason would have survived. And **where a rule is corrected rather than the code, say which way round it went**, because the next reader will otherwise re-derive the original objection — which is exactly what happened here, correctly, and cost a round.
|
||||
|
||||
**And a second occurrence of the entry above it, which is what turns one chart's story into a rule.** `stalwart`'s per-entry `tlsSecretName` check had exactly the defect the realm-list round describes: with **both** routing modes on and a `tlsSecretName` missing, the render reported *"in Ingress mode every entry needs one"* — true, and the wrong diagnosis, because that configuration is not coherent to begin with. Found by rendering the conflicting combination rather than by reading the template, and fixed the same way: the Ingress-mode block is gated on `ingress.enabled` **and not** `httpRoute.enabled`, so in that state it validates nothing and the only `fail` left is the structural one. Two charts met it in the same round, from two agents, neither having seen the other's code — so expect it in the next chart that gains a per-entry check beside a mode switch, and put the conflicting combination in the test matrix for the new value.
|
||||
+7
-1
@@ -330,6 +330,8 @@ Added with Gitea #123. Stalwart serves the MTA-STS policy itself, per domain and
|
||||
| Ingress | **one Ingress per distinct `clusterIssuer`** — `<release>-stalwart-mta-sts-<issuer>-ingress`, and `<release>-stalwart-mta-sts-ingress` for the issuer-less group — each with one rule per host and its `tls:` list grouped by secret |
|
||||
| Gateway | **one `HTTPRoute`**, `<release>-stalwart-mta-sts-httproute`, carrying every hostname. Nothing differs per host (same path, same backend, no filter), and it attaches to the same Gateway(s) as the WebUI route |
|
||||
|
||||
**"TLS belongs to the platform" is not the same as "there is no mechanism for it."** In Gateway mode this chart renders no certificate for these hostnames and both TLS fields are inert — but cert-manager in this cluster runs with **`enableGatewayAPI: true`** (measured in its `ControllerConfiguration`), so it can issue the **listener** certificates itself, from an annotation on the Gateway rather than on anything here. So the work does not disappear with the Ingress, it moves to the Gateway object — and mind the depth trap below while doing it, because a wildcard listener for the parent domain will not cover `mta-sts.sub.<domain>`.
|
||||
|
||||
**The path is not a value.** RFC 8461 fixes it at `/.well-known/mta-sts.txt`, rendered with `pathType: Exact` (`type: Exact` in the route) because exactly one file lives there. A values key for a protocol-mandated path is one that can only ever be set wrong — and wrong is invisible from this side, see the traps below. `Exact` also matters in its own right: a prefix on these hostnames would publish the mail server's whole management surface under them.
|
||||
|
||||
The chart's own `ingress.annotations` are **not** copied onto these objects. They belong to the WebUI/JMAP host — on this installation they are its CORS configuration — and a policy file served to sending MTAs has no use for them.
|
||||
@@ -577,7 +579,11 @@ Certificate tls-mta-sts-bekemeyer-klein-de-ingress
|
||||
|
||||
**ingress-shim names the Certificate after the secret** and owns it from the Ingress. Two Ingress objects whose `tls` entries name the same secret therefore drive the shim at **one** Certificate name from two owners — so a shared secret must sit in exactly one object (hence the grouping), and a secret may **not** span two issuer groups (hence the abort).
|
||||
|
||||
That is also why the object names carry the **issuer** and not the entry's index. [homeassistant](homeassistant) names its routes by index and writes down that reordering the list recreates them — *"harmless (a route holds no state or certificate)"*. These objects **do** hold a certificate, so the exception that precedent names itself applies here.
|
||||
**The objects are named by index all the same** — `…-mta-sts-ingress`, `…-mta-sts-ingress-1`, … — in the repo's idiom ([style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api)), as [homeassistant](homeassistant)'s routes and [keycloak](keycloak)'s realm Ingresses do it. The grouping above and the naming are independent decisions. An earlier draft of this chart named them by **issuer**, arguing that the index rule rested on *"a route holds no state or certificate"* and that these objects hold one. The reading was right and **the rule's stated reason was wrong** — it was the reason that got corrected, not the naming of two charts, because `keycloak`'s realm Ingresses own certificates too and are already published. What actually carries the index rule is that **the Secret survives a rename**, so no certificate is re-issued; the three-step version, and the cluster flag it depends on, is in that style-guide section.
|
||||
|
||||
**So the name does not say which issuer an object carries.** `NOTES.txt` prints the index → issuer → hosts mapping, and it is the only place that association is visible — read it there rather than inferring it from object names.
|
||||
|
||||
**And reordering `mtaSts.hosts` is sharper here than at `homeassistant`.** There an index names a host. Here it names an **issuer group derived from the list**, so moving one entry can change how many objects exist and which index holds which issuer — renumbering objects that were not themselves moved. Kubernetes recreates them, which takes their Certificate objects along; **nothing is re-issued, because the Secrets survive**, and that holds only while the cluster leaves `--enable-certificate-owner-ref` unset. Measured on this cluster: it is unset. It is a property of the **cluster**, not of this chart.
|
||||
|
||||
### The Ingress ban trap
|
||||
|
||||
|
||||
Reference in new issue
Block a user