--- date: 2026-11-07 branch: feat/l0-externaldns-annotation-prefix title: L0 guard — external-dns annotationPrefix must match every external-dns annotation in the tree --- ## What changed `conformance/tests/l0-static/internal-services-publish-no-public-dns/check.sh` gains assertion (8). The check is already whole-repo-scoped, so it runs on every gate. - **(8a)** `platform/externaldns/helmrelease.yaml` must set `values.annotationPrefix` explicitly, and it must equal the check's `ANNOTATION_PREFIX` (`external-dns.alpha.kubernetes.io/`). The check does not trust the upstream default, because that default is what changed. - **(8b)** `extraArgs` must not carry `--annotation-prefix` or `--enable-legacy-annotation-prefix`, so the prefix has one declared place. - **(8c)** Every external-dns-looking annotation key under apps/, platform/ and runbooks/ must use that prefix. The match is any `external-dns(.x)*.kubernetes.io/` key. - **(8d)** catalog/render.py must emit no external-dns key under any other prefix. `CONTROLLER_KEY` and `HOSTNAME_KEY` are now derived from `ANNOTATION_PREFIX`. The Service inventory matches any external-dns prefix variant, not only the alpha one. Why: on 2026-11-07, external-dns v0.23.0 (whose default prefix is GA) ignored every alpha annotation. Assertions (1) to (3) stayed green, because they prove the annotations are present, not that the controller reads them. See `decisions/2026-11-07-public-dns-wan-a-record.md`. This branch is stacked on `fix/public-dns-wan-a-record` (PR #140). Without that fix, (8a) fails, which is correct. Merge #140 first. ## What I verified Run on the host (PyYAML): - the check passes on this tree: `check OK: 62 assertions passed`. - it **fails (8a)** with origin/main's HelmRelease, which has no `annotationPrefix`. - it **fails (8c)** when `platform/gateway/gateway.yaml`'s `target` key is changed to the GA prefix. Toolbox gate: see the PR description. ## What I did NOT verify - Whether a future chart could read the prefix from somewhere other than `values.annotationPrefix`, for example a renamed chart value. (8a) would then pass while the flag silently vanished from the rendered args. Rendering the chart in the gate would close that gap. Not done: it needs network access or a vendored chart in L0. - `conformance/**` is a **sensitive path**. This PR needs the operator's approving review or `/approve-override`. The agent did not post one. - The header of the sibling chainsaw-test.yaml still says "external-dns v0.21.0". That is cosmetic and I left it alone.