sync dari sicabe-doc/deploy @ e8f8fde

This commit is contained in:
2026-10-11 18:13:03 +07:00
parent 6e4dfb69ef
commit 9f454b6e8e
12 changed files with 191 additions and 29 deletions
+42 -5
View File
@@ -96,6 +96,30 @@ PUBLIC_URL_ENDPOINTS = {
"MINIO_CONSOLE_PUBLIC_URL": ("minio", "http://{addr}:12901"),
}
def _ext_url(ext: str, template: str, port: str = "") -> str:
"""URL untuk komponen EKSTERNAL dari `components.<c>.address`.
Bila operator menulis **URL penuh** (mengandung `://`, mis.
`https://sso.example.com`) → pakai APA ADANYA: skema + host + port dihormati
(default port per skema, mis. https→443, TIDAK ditulis bila default). Bila hanya
host/IP polos (perilaku lama) → susun dari `template` + `port` seperti sebelumnya.
"""
scheme, host, p = inventory.parse_address(ext)
if scheme:
# Buang port bila = default skema (https→443), supaya `https://host`, bukan
# `https://host:443` (ekuivalen, tapi lebih bersih & cocok utk issuer/URL publik).
show = p and p != inventory._DEFAULT_SCHEME_PORT.get(scheme)
return f"{scheme}://{host}" + (f":{p}" if show else "")
if port and "{port}" in template:
return template.format(addr=host, port=(p if p else port))
return template.format(addr=host)
def _keycloak_external_scheme(doc: dict) -> str:
"""Skema URL Keycloak eksternal ('' bila bukan URL penuh). Untuk sslRequired."""
return inventory.parse_address(inventory.external_address(doc, "keycloak") or "")[0]
# SENGAJA TIDAK diarahkan ke IP: KEYCLOAK_URL / TRINO_URL dan var `*_URL` lain yang
# bersifat identitas (issuer OIDC harus stabil — lihat R10 §4.10.1: pakai DNS
# internal, bukan IP). Itu milik operator dan harus sudah benar di `cluster.yml`
@@ -195,7 +219,7 @@ def _is_topology_var(line: str) -> bool:
"""
key = line.split("=", 1)[0].strip()
owned = {
"EXTERNAL_SERVICES", "LOCAL_SERVICES", "KEYCLOAK_SSL_REQUIRED",
"EXTERNAL_SERVICES", "LOCAL_SERVICES", "KEYCLOAK_SSL_REQUIRED", "KEYCLOAK_INTERNAL_URL",
"KC_PROXY_HEADERS", "TRINO_PROCESS_FORWARDED", "CLUSTER_DOMAIN", "TRAEFIK_BIND_ADDR",
*ENDPOINT_VARS, *URL_ENDPOINT_VARS,
*inventory.PUBLIC_URL_SERVICES, # KEYCLOAK_PUBLIC_URL, GRAFANA_PUBLIC_URL, …
@@ -458,7 +482,10 @@ def endpoint_lines(doc: dict, host: str, plan: dict) -> list[str]:
for var, component in ENDPOINT_VARS.items():
ext = inventory.external_address(doc, component)
if ext:
lines.append(f"{var}={ext}") # EKSTERNAL (pihak lain) → alamat tetap
# EKSTERNAL → HOST saja (buang skema/port bila operator menulis URL penuh,
# mis. `https://sso.example.com` → `sso.example.com`). URL lengkapnya
# ditangani `*_URL` / KEYCLOAK_INTERNAL_URL, bukan var `*_HOST`.
lines.append(f"{var}={inventory.parse_address(ext)[1]}")
continue
owners = placement.get(component)
if not owners:
@@ -467,6 +494,13 @@ def endpoint_lines(doc: dict, host: str, plan: dict) -> list[str]:
lines.append(f"{var}={component}") # SE-HOST → nama service
else:
lines.append(f"{var}={plan[owners[0]]['address']}") # LINTAS host → alamat
# Keycloak: URL server→server (token/jwks/Grafana). Hanya ditulis bila Keycloak
# EKSTERNAL memakai URL PENUH (mis. https://sso.example.com) agar skema+port
# dihormati; selain itu compose/script memakai default
# `http://${KEYCLOAK_HOST:-keycloak}:${KEYCLOAK_INTERNAL_PORT:-8080}`.
kc_ext = inventory.external_address(doc, "keycloak")
if kc_ext and inventory.parse_address(kc_ext)[0]:
lines.append(f"KEYCLOAK_INTERNAL_URL={_ext_url(kc_ext, '')}")
disabled = [n for n, v in inventory.enabled_components(doc).items() if v is False]
if disabled:
# Komponen `enabled: false` = tidak dijalankan di host mana pun → installer
@@ -498,7 +532,7 @@ def public_url_lines(doc: dict, plan: dict | None = None) -> list[str]:
for var, (component, tmpl) in PUBLIC_URL_ENDPOINTS.items():
ext = inventory.external_address(doc, component)
if ext:
resolved[var] = tmpl.format(addr=ext) # eksternal → alamat tetap
resolved[var] = _ext_url(ext, tmpl) # eksternal → URL penuh dihormati
continue
owners = placement.get(component)
if not owners:
@@ -523,8 +557,11 @@ def tls_policy_lines(doc: dict) -> list[str]:
tls.mode `edge`/`full` -> `external` (ada Traefik/TLS di depan)
"""
edge = inventory.tls_mode(doc) != "none"
# Keycloak EKSTERNAL ber-URL `https://…` juga menuntut sslRequired=external (permintaan
# dari alamat non-lokal lewat TLS di depannya).
ext_https = _keycloak_external_scheme(doc) == "https"
return [
f"KEYCLOAK_SSL_REQUIRED={'external' if edge else 'none'}",
f"KEYCLOAK_SSL_REQUIRED={'external' if (edge or ext_https) else 'none'}",
# Di belakang Traefik, Keycloak WAJIB memercayai X-Forwarded-* — tanpa itu
# permintaan HTTPS dari proxy dianggap HTTP (redirect salah / tuntutan HTTPS).
# Sengaja KOSONG saat tanpa proxy: memasangnya membuat Keycloak memercayai
@@ -665,7 +702,7 @@ def url_endpoint_lines(doc: dict, host: str, plan: dict, env_path=None) -> list[
for var, (component, template) in URL_ENDPOINT_VARS.items():
ext = inventory.external_address(doc, component)
if ext:
lines.append(f"{var}={template.format(addr=ext, port=_endpoint_port(var, env_path))}")
lines.append(f"{var}={_ext_url(ext, template, _endpoint_port(var, env_path))}")
continue
owners = placement.get(component)
if not owners:
+41
View File
@@ -145,6 +145,47 @@ def external_address(doc: dict, service: str) -> str | None:
return a or None
# Port default per skema — dipakai parse_address() bila operator menulis URL penuh
# TANPA port (mis. `https://sso.example.com` → port 443).
_DEFAULT_SCHEME_PORT = {
"https": 443, "http": 80, "ldaps": 636, "ldap": 389,
"postgres": 5432, "postgresql": 5432,
}
def parse_address(addr: str) -> tuple[str, str, int | None]:
"""Pisah alamat komponen → (skema, host, port).
Menerima dua bentuk:
- **URL penuh** (skema eksplisit): `https://sso.example.com` → ('https','sso.example.com',443)
`http://minio.corp:9000` → ('http','minio.corp',9000)
- **host/IP polos** (perilaku lama): `sso.example.com` → ('','sso.example.com',None)
`10.20.0.10` → ('','10.20.0.10',None)
Skema non-standar diterima apa adanya; path (`/foo`) dibuang.
"""
a = (addr or "").strip()
scheme = ""
if "://" in a:
scheme, a = a.split("://", 1)
scheme = scheme.strip().lower()
a = a.split("/", 1)[0] # buang path bila ada
host, sep, port_s = a.rpartition(":")
# host:port HANYA bila tepat satu ':' dan port numerik — melindungi IPv6
# (mis. `2001:db8::1`) agar tak salah dipecah.
if sep and port_s.isdigit() and a.count(":") == 1:
host, port = host, int(port_s)
else:
host, port = a, _DEFAULT_SCHEME_PORT.get(scheme)
return scheme, host, port
def external_host(doc: dict, service: str) -> str | None:
"""HOST saja (tanpa skema/port) dari alamat eksternal — untuk var `*_HOST`."""
a = external_address(doc, service)
return parse_address(a)[1] if a else None
def resolve(doc: dict) -> dict[str, list[str]]:
"""Komponen → daftar host (tier `engine` bisa >1 host). Komponen EKSTERNAL
(ber-`address`) TIDAK masuk hasil — ia bukan milik host mana pun."""