Files
fn_registry/python/functions/pipelines/refresh_local_hub.py
T
egutierrez 32c7336bf6 feat(infra): auto-commit con 56 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-21 14:22:55 +02:00

209 lines
7.4 KiB
Python

"""refresh_local_hub — orquesta el refresco del sistema local_hub.
Pipeline impuro del dominio `pipelines`. Compone tres funciones del registry para
regenerar la infraestructura que expone los servicios locales como subdominios
`*.localhost`:
1. discover_local_services_py_infra — descubre y normaliza los servicios (manifiesto
+ servicios del registry con bloque `service:`), comprobando estado up/down.
2. render_caddyfile_py_infra — genera el fragmento de Caddyfile.
3. render_glance_config_py_infra — genera la config del widget monitor de Glance.
Después escribe el fragmento de Caddyfile en /etc/caddy/conf.d/local_hub.caddy (el
usuario tiene ACL de escritura ahí, sin sudo) y la config de Glance en
apps/local_hub/glance/glance.yml. Si `reload=True`, recarga Caddy (admin API en
localhost:2019, sin sudo) y reinicia la user-unit `glance` (sin sudo).
Pensado para correrse a diario desde dag_engine con un step `function:`, o a mano:
`fn run refresh_local_hub`.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from typing import Any
import yaml
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "python", "functions"))
from infra.discover_local_services import discover_local_services # noqa: E402
from infra.render_caddyfile import render_caddyfile # noqa: E402
from infra.render_glance_config import render_glance_config # noqa: E402
# Ruta del fragmento de Caddyfile. El usuario tiene ACL de escritura aquí (sin sudo).
CADDY_FRAGMENT_PATH = "/etc/caddy/conf.d/local_hub.caddy"
# Bloque fijo de servidor + tema que precede a la salida de render_glance_config.
GLANCE_SERVER_BLOCK = (
"server:\n"
" host: 127.0.0.1\n"
" port: 8585\n"
"\n"
"theme:\n"
" background-color: 240 8 9\n"
" contrast-multiplier: 1.2\n"
" primary-color: 210 90 70\n"
"\n"
)
def _default_manifest_path() -> str:
"""Ruta por defecto del manifiesto, derivada de la raíz del registry."""
root = os.environ.get("FN_REGISTRY_ROOT") or ROOT
return os.path.join(root, "apps", "local_hub", "local_services.yaml")
def _registry_root() -> str:
"""Raíz del registry: FN_REGISTRY_ROOT si está, si no la derivada del path del módulo."""
return os.environ.get("FN_REGISTRY_ROOT") or ROOT
def refresh_local_hub(
manifest_path: str | None = None,
reload: bool = True,
) -> dict[str, Any]:
"""Refresca el sistema local_hub: descubre servicios, regenera configs y recarga.
Args:
manifest_path: ruta al manifiesto YAML del local_hub. Si es None, se usa
``<RAIZ>/apps/local_hub/local_services.yaml`` (RAIZ derivada de
FN_REGISTRY_ROOT o del path del propio módulo).
reload: si True, recarga Caddy (admin API en localhost:2019) y reinicia la
user-unit ``glance``. Si False, solo escribe las configs y no toca
ningún servicio.
Returns:
dict resumen con las claves: total, up, down, caddy_path, glance_path,
reloaded, caddy_reload_rc, glance_restart_rc, services.
Raises:
RuntimeError: si la config de Glance generada no es YAML parseable.
"""
if manifest_path is None:
manifest_path = _default_manifest_path()
# 1. Lee el manifiesto para extraer dashboard_subdomain y glance_port.
try:
with open(manifest_path, "r", encoding="utf-8") as fh:
manifest = yaml.safe_load(fh) or {}
except (OSError, yaml.YAMLError) as exc:
raise RuntimeError(
f"refresh_local_hub: no se puede leer el manifiesto {manifest_path}: {exc}"
) from exc
dashboard_subdomain = manifest.get("dashboard_subdomain") or "home"
glance_port = manifest.get("glance_port") or 8585
# 2. Descubre los servicios (manifiesto + registry).
services = discover_local_services(manifest_path, include_registry=True)
# 3. Bloque del dashboard para el Caddyfile.
dashboard = {"subdomain": dashboard_subdomain, "port": glance_port}
# 4. Renderiza el fragmento de Caddyfile.
caddy_text = render_caddyfile(services, dashboard)
# 5. Construye la config completa de Glance (bloque fijo + render_glance_config).
glance_full = GLANCE_SERVER_BLOCK + render_glance_config(services)
# Verifica que el YAML resultante es parseable antes de escribir nada.
try:
yaml.safe_load(glance_full)
except yaml.YAMLError as exc:
raise RuntimeError(
f"refresh_local_hub: la config de Glance generada no es YAML válido: {exc}"
) from exc
# 6. Escribe el fragmento de Caddyfile (ACL del usuario, sin sudo).
with open(CADDY_FRAGMENT_PATH, "w", encoding="utf-8") as fh:
fh.write(caddy_text)
# 7. Escribe la config de Glance (crea el dir si falta).
glance_path = os.path.join(_registry_root(), "apps", "local_hub", "glance", "glance.yml")
os.makedirs(os.path.dirname(glance_path), exist_ok=True)
with open(glance_path, "w", encoding="utf-8") as fh:
fh.write(glance_full)
up = sum(1 for s in services if s.get("up"))
down = len(services) - up
caddy_reload_rc: int | None = None
glance_restart_rc: int | None = None
# 8. Recarga Caddy y reinicia Glance (ambos sin sudo).
if reload:
try:
caddy_proc = subprocess.run(
["caddy", "reload", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"],
capture_output=True,
text=True,
timeout=30,
)
caddy_reload_rc = caddy_proc.returncode
except (OSError, subprocess.SubprocessError) as exc:
caddy_reload_rc = -1
sys.stderr.write(f"refresh_local_hub: fallo recargando Caddy: {exc}\n")
try:
glance_proc = subprocess.run(
["systemctl", "--user", "restart", "glance"],
capture_output=True,
text=True,
timeout=30,
)
glance_restart_rc = glance_proc.returncode
except (OSError, subprocess.SubprocessError) as exc:
glance_restart_rc = -1
sys.stderr.write(f"refresh_local_hub: fallo reiniciando Glance: {exc}\n")
return {
"total": len(services),
"up": up,
"down": down,
"caddy_path": CADDY_FRAGMENT_PATH,
"glance_path": glance_path,
"reloaded": reload,
"caddy_reload_rc": caddy_reload_rc,
"glance_restart_rc": glance_restart_rc,
"services": [
{
"name": s.get("name"),
"subdomain": s.get("subdomain"),
"port": s.get("port"),
"up": s.get("up"),
}
for s in services
],
}
def main() -> None:
parser = argparse.ArgumentParser(description="Refresca el sistema local_hub.")
parser.add_argument(
"--manifest-path",
default=None,
help="Ruta al manifiesto YAML del local_hub (default: <RAIZ>/apps/local_hub/local_services.yaml).",
)
parser.add_argument(
"--no-reload",
action="store_true",
help="No recargar Caddy ni reiniciar Glance; solo regenerar las configs.",
)
args = parser.parse_args()
result = refresh_local_hub(
manifest_path=args.manifest_path,
reload=not args.no_reload,
)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()