test(k8s): pin the issuer, the portal authority and the public edge (refs #177)
Keycloak pins one issuer and each portal is configured with one authority; when they drift the symptom lands three services away — a login that bounces back logged out, or a 401 from the BFF (ADR-0010) — so assert they are the same string. The same check states what publishing the stack has to mean: with `public.domain` set the five hostnames are served and both halves become `https://auth.<domain>`, and with it empty nothing of the edge renders, which is what compose, CI and a laptop cluster depend on. Red: the chart has no `public.domain`, so setting it changes nothing and no hostname is published. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,7 @@ K8S_IMAGES := acl domain bff event-subscriber projection-api self-service open
|
|||||||
k8s-lint:
|
k8s-lint:
|
||||||
helm lint $(K8S_CHART)
|
helm lint $(K8S_CHART)
|
||||||
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
|
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
|
||||||
|
python3 infra/helm/check-issuer.py
|
||||||
|
|
||||||
## k8s-drift: fail if compose and the Helm chart describe different stacks
|
## k8s-drift: fail if compose and the Helm chart describe different stacks
|
||||||
# Compose is CI-canonical (ADR-0033) and the chart is a transcription of it; this
|
# Compose is CI-canonical (ADR-0033) and the chart is a transcription of it; this
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail when the pinned issuer and the portals' OIDC authority stop agreeing.
|
||||||
|
|
||||||
|
Keycloak pins one issuer (`KC_HOSTNAME`) and each portal is configured with one
|
||||||
|
authority (`config.json`). A browser token carries the first; the BFF validates
|
||||||
|
against what it discovers from the second (ADR-0010). When the two drift the
|
||||||
|
symptom is three services away — a login that bounces back logged out, or a 401
|
||||||
|
from the BFF — so the chart builds both from one helper and this asserts it.
|
||||||
|
|
||||||
|
It also pins the two halves of the public edge (ADR-0035): that setting
|
||||||
|
`public.domain` actually publishes the hostnames, and that leaving it empty
|
||||||
|
renders no edge at all, which is what compose, CI and a laptop cluster rely on.
|
||||||
|
|
||||||
|
Run it with `make k8s-lint`. No cluster needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CHART = Path(__file__).resolve().parent / "big-reference"
|
||||||
|
DOMAIN = "example.test"
|
||||||
|
|
||||||
|
|
||||||
|
def render(*sets: str) -> str:
|
||||||
|
argv = ["helm", "template", "big", str(CHART), "-n", "big"]
|
||||||
|
for s in sets:
|
||||||
|
argv += ["--set", s]
|
||||||
|
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
sys.exit(f"helm template failed:\n{proc.stderr}")
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def issuer(out: str) -> str:
|
||||||
|
"""The value of KC_HOSTNAME in the rendered manifests."""
|
||||||
|
m = re.search(r"name: KC_HOSTNAME\n\s+value: \"(\S+)\"", out)
|
||||||
|
return m[1] if m else ""
|
||||||
|
|
||||||
|
|
||||||
|
def authorities(out: str) -> set[str]:
|
||||||
|
"""Every portal's OIDC authority, with the realm path stripped."""
|
||||||
|
found = set()
|
||||||
|
for line in re.findall(r'\{ "authority": .* \}', out):
|
||||||
|
url = json.loads(line)["authority"]
|
||||||
|
found.add(url.rsplit("/realms/", 1)[0])
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
problems = []
|
||||||
|
|
||||||
|
public = render(f"public.domain={DOMAIN}")
|
||||||
|
if issuer(public) != f"https://auth.{DOMAIN}":
|
||||||
|
problems.append(f" with public.domain set, KC_HOSTNAME is {issuer(public)!r}, not https://auth.{DOMAIN}")
|
||||||
|
if authorities(public) != {f"https://auth.{DOMAIN}"}:
|
||||||
|
problems.append(f" with public.domain set, the portals point at {sorted(authorities(public))}")
|
||||||
|
for host in (f"register.{DOMAIN}", f"mijn.{DOMAIN}", f"behandel.{DOMAIN}", f"beheer.{DOMAIN}", f"auth.{DOMAIN}"):
|
||||||
|
if host not in public:
|
||||||
|
problems.append(f" {host} is not published by the edge")
|
||||||
|
|
||||||
|
private = render()
|
||||||
|
if authorities(private) != {issuer(private)}:
|
||||||
|
problems.append(f" by default the portals point at {sorted(authorities(private))}, the issuer is {issuer(private)!r}")
|
||||||
|
if "caddy-edge" in private:
|
||||||
|
problems.append(" the edge renders with no public.domain — compose, CI and a laptop cluster expect nothing")
|
||||||
|
|
||||||
|
if problems:
|
||||||
|
print("the chart's OIDC origin is inconsistent:\n" + "\n".join(problems))
|
||||||
|
print("\nBoth halves come from the `big.keycloakUrl` helper — change it, not one caller.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"issuer + portal authority agree, with and without a public domain")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user