Files
home-network-k3s-impl/platform/keycloak/client-urls.yaml
jakman85andClaude Opus 5 51d8676f90 feat(keycloak): render the client-URL desired state from the realm import
The realm import is a one-shot: once Done=True it never re-runs, so a redirect URI
changed in git never reaches the realm. Healing that needs a DESIRED STATE the
reconciler can read -- and the reconciler pod has no Kubernetes API access at all
(automountServiceAccountToken: false), so it cannot read the KeycloakRealmImport CR.
Its desired state must be mounted, hence a ConfigMap.

A hand-authored ConfigMap would be a second source of truth for every public
hostname in the realm, free to disagree with the import -- the same shape as the bug
being closed. So it is GENERATED from the import and the render is gated, the same
render==golden idiom catalog/render.py already uses for apps/.

URL strings are copied VERBATIM: a ${BASE_DOMAIN} token in the source appears
unchanged here, and both files are rendered by the SAME Flux Kustomization
(./platform-config), so both are substituted in one pass with one value. The
renderer never parses a hostname and never reads cluster-vars, so there is no
second place the domain can be decided.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-12 21:24:52 -05:00

158 lines
4.9 KiB
YAML

---
# =============================================================================
# GENERATED FILE — DO NOT EDIT BY HAND.
#
# source: platform/keycloak/keycloak-realm-import.yaml (spec.realm.clients)
# regenerate: python3 ci/render-keycloak-client-urls.py --write
# gated by: conformance/tests/l0-static/keycloak-client-secret-reconciler
# (runs this renderer with --check on every merge)
#
# WHAT THIS IS: the DESIRED STATE for every environment-derived client URL in the
# `homelab` realm — redirectUris, webOrigins, rootUrl, baseUrl, adminUrl. The
# client-secret reconciler CronJob mounts it and makes the live realm match it.
#
# WHY IT IS A SEPARATE OBJECT: the realm import is a ONE-SHOT that never re-runs
# once Done=True, so a redirect URI edited in git never reaches the realm. Prod's
# stored redirect URIs were measured still pointing at the pre-migration hostname
# while git declared the new one, and every OIDC login would have failed with a
# redirect_uri mismatch. The reconciler is the level-triggered loop that closes
# that; this file is what it reconciles TOWARDS.
#
# WHY IT IS GENERATED AND NOT AUTHORED: a hand-written copy of every public
# hostname would be a second source of truth, free to disagree with the realm
# import. It is rendered from the import and the render is gated, so the two
# cannot drift. URL strings are copied VERBATIM — if the source uses a
# ${BASE_DOMAIN} token it appears here unchanged, and both files are substituted
# by the SAME Flux Kustomization (./platform-config), so the domain is decided in
# exactly one place.
#
# A field that is NOT declared on a client in the realm import is simply ABSENT
# here, and the reconciler leaves it alone. Absence means UNMANAGED, never
# "force to empty".
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-client-urls
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-client-urls
app.kubernetes.io/part-of: keycloak
data:
client-urls.json: |
{
"crafty": {
"redirectUris": [
"https://crafty.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://crafty.${BASE_DOMAIN}"
]
},
"gitea": {
"redirectUris": [
"https://gitea.${BASE_DOMAIN}/user/oauth2/keycloak/callback"
],
"webOrigins": [
"https://gitea.${BASE_DOMAIN}"
]
},
"grafana": {
"redirectUris": [
"https://grafana.${BASE_DOMAIN}/login/generic_oauth"
],
"webOrigins": [
"https://grafana.${BASE_DOMAIN}"
]
},
"headlamp": {
"redirectUris": [
"https://headlamp.${BASE_DOMAIN}/oidc-callback"
],
"webOrigins": [
"https://headlamp.${BASE_DOMAIN}"
]
},
"jackett": {
"redirectUris": [
"https://jackett.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://jackett.${BASE_DOMAIN}"
]
},
"kubernetes": {
"redirectUris": [
"http://localhost:8000",
"http://localhost:8000/",
"http://localhost:18000",
"http://localhost:18000/",
"http://127.0.0.1:8000",
"http://127.0.0.1:8000/",
"http://127.0.0.1:18000",
"http://127.0.0.1:18000/"
],
"webOrigins": [
"http://localhost",
"http://127.0.0.1"
]
},
"nextcloud": {
"redirectUris": [
"https://nextcloud.${BASE_DOMAIN}/apps/user_oidc/code"
],
"webOrigins": [
"https://nextcloud.${BASE_DOMAIN}"
]
},
"oauth2-proxy": {
"redirectUris": [
"https://*.${BASE_DOMAIN}/oauth2/callback",
"https://auth.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://*.${BASE_DOMAIN}"
]
},
"pihole": {
"redirectUris": [
"https://pihole.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://pihole.${BASE_DOMAIN}"
]
},
"qbittorrent": {
"redirectUris": [
"https://qbittorrent.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://qbittorrent.${BASE_DOMAIN}"
]
},
"radarr": {
"redirectUris": [
"https://radarr.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://radarr.${BASE_DOMAIN}"
]
},
"semaphore": {
"redirectUris": [
"https://semaphore.${BASE_DOMAIN}/api/auth/oidc/keycloak/redirect"
],
"webOrigins": [
"https://semaphore.${BASE_DOMAIN}"
]
},
"sonarr": {
"redirectUris": [
"https://sonarr.${BASE_DOMAIN}/oauth2/callback"
],
"webOrigins": [
"https://sonarr.${BASE_DOMAIN}"
]
}
}