"""Renderiza un fragmento de Caddyfile para el sistema local_hub. Funcion pura: transforma una lista de servicios normalizados en el texto de un Caddyfile que mapea cada subdominio `*.localhost` a su puerto local via reverse_proxy HTTP plano (loopback, sin TLS). Sin I/O, sin red, determinista. """ _HEADER = ( "# Generado por render_caddyfile_py_infra — NO editar a mano. " "Fuente: apps/local_hub/local_services.yaml\n" ) def _render_block(subdomain: str, port: int, rewrite_host: bool = False) -> str: """Construye un bloque reverse_proxy para un subdominio y puerto dados. Args: subdomain: subdominio sin el sufijo `.localhost` (ej. "metabase"). port: puerto local al que redirigir (ej. 3030). rewrite_host: si es True, reescribe la cabecera `Host` enviada al upstream a `127.0.0.1:`. Necesario para servicios que validan el header Host y rechazan el subdominio (ej. Jupyter devuelve 400 "Bad Request" si recibe `Host: jupyter.localhost`). Returns: el bloque Caddyfile como string, con indentacion de 4 espacios y terminado en `\n`. """ if rewrite_host: return ( f"http://{subdomain}.localhost {{\n" f" reverse_proxy 127.0.0.1:{port} {{\n" f" header_up Host 127.0.0.1:{port}\n" f" }}\n" f"}}\n" ) return ( f"http://{subdomain}.localhost {{\n" f" reverse_proxy 127.0.0.1:{port}\n" f"}}\n" ) def render_caddyfile(services: list[dict], dashboard: dict | None = None) -> str: """Renderiza el texto de un fragmento de Caddyfile para local_hub. Cada servicio se mapea a un bloque `http://.localhost` con un `reverse_proxy 127.0.0.1:`. Los bloques de servicio se ordenan por subdominio alfabetico para que la salida sea estable y reproducible. El bloque del dashboard, si se pasa, va siempre primero (es la pagina principal). Se usa HTTP plano a proposito: todo es loopback, no hay TLS. Args: services: lista de dicts de servicio. Cada uno debe tener al menos `subdomain` (str) y `port` (int); otras claves se ignoran salvo `rewrite_host` (bool opcional): si es truthy, el bloque reescribe la cabecera `Host` enviada al upstream a `127.0.0.1:` (para servicios que validan Host, ej. Jupyter). Los servicios sin `subdomain` o sin `port` se saltan (no lanzan error). No se deduplica (eso es trabajo del discover). dashboard: dict opcional con `subdomain` y `port` para la pagina principal. Si es None, no se emite bloque de dashboard. Si le falta `subdomain` o `port`, se ignora igual que un servicio invalido. Returns: el Caddyfile completo como string: empieza por una cabecera de comentario, luego (si aplica) el bloque del dashboard, y despues los bloques de servicio ordenados. Termina con un unico `\n`. """ parts: list[str] = [_HEADER] if dashboard is not None: d_sub = dashboard.get("subdomain") d_port = dashboard.get("port") if d_sub is not None and d_port is not None: parts.append(_render_block(d_sub, d_port)) valid = [ svc for svc in services if svc.get("subdomain") is not None and svc.get("port") is not None ] for svc in sorted(valid, key=lambda s: s["subdomain"]): parts.append( _render_block(svc["subdomain"], svc["port"], bool(svc.get("rewrite_host"))) ) return "".join(parts)