import html from urllib.parse import quote from fastapi import APIRouter, Depends, Form, HTTPException from fastapi.responses import RedirectResponse from app.auth import require_user from app.db.audit import log_audit_event from app.db.jobs import create_job router = APIRouter() # Samostatná stránka "Runtime Management" byla odstraněna – překrývala se s Environment, odkud se # tyto redeploy akce spouští. Zůstává jen backend: render_action_buttons() + akční endpoint, které # používá stránka Environment (app/routes/environment.py). # # Portál NEMÁ vlastní deploy logiku. Všechny provozní akce deleguje do existujících shell skriptů # v appfactory-tools přes existující job systém (job_type="run_script"). # Skripty leží v appfactory-tools/scripts/, proto je předáváme i s prefixem scripts/, jinak je # worker hledá ve špatné složce a job selže s "skript nenalezen". DEPLOY_CORE_SCRIPT = "scripts/deploy-core-service.sh" CADDY_SCRIPT = "scripts/generate-caddyfile.sh" ENABLED_APPS_SCRIPT = "scripts/redeploy-enabled-apps.sh" ALL_CORE_KEY = "redeploy-all-core" ALL_CORE_LABEL = "Redeploy All Core Services" ALL_CORE_ICON = "fa-server" # Cílová stránka pro návrat po akci (dříve Runtime Management, nyní Environment). DEFAULT_ACTION_NEXT = "/portal/admin/environment" # Core služby nasazované přes deploy-core-service.sh . (key, name, icon, service_arg) CORE_SERVICES = [ ("redeploy-portal", "Portal", "fa-window-maximize", "appfactory-portal"), ("redeploy-worker", "Worker", "fa-gears", "appfactory-worker"), ("redeploy-webhook", "Webhook", "fa-bolt", "appfactory-webhook"), ("redeploy-monitor", "Monitor", "fa-heart-pulse", "appfactory-monitor"), ("redeploy-gateway", "Gateway (Caddy)", "fa-shield-halved", "appfactory-caddy"), ("redeploy-gitea", "Gitea", "fa-code-branch", "appfactory-gitea"), ("redeploy-registry", "Registry", "fa-box-archive", "appfactory-registry"), ] # Spustitelné akce: key -> (label, icon, script_name, args). ALL_CORE_KEY je speciální (smyčka přes core). ACTION_MAP: dict[str, tuple[str, str, str, list[str]]] = {} for _key, _name, _icon, _arg in CORE_SERVICES: ACTION_MAP[_key] = (f"Redeploy {_name}", _icon, DEPLOY_CORE_SCRIPT, [_arg]) ACTION_MAP["regenerate-caddy"] = ("Regenerate Caddy", "fa-shield-halved", CADDY_SCRIPT, []) ACTION_MAP["redeploy-enabled-apps"] = ("Redeploy Enabled Apps", "fa-rocket", ENABLED_APPS_SCRIPT, []) # Pořadí tlačítek po uložení .env (Environment stránka) – dle zadání. ENV_ACTION_KEYS = [ "regenerate-caddy", "redeploy-portal", "redeploy-worker", "redeploy-webhook", "redeploy-monitor", "redeploy-gateway", "redeploy-gitea", "redeploy-registry", ALL_CORE_KEY, ] def require_admin(user: dict) -> None: if (user.get("role") or "").lower() != "admin": raise HTTPException(status_code=403, detail="Runtime akce jsou dostupné pouze administrátorům") def _is_valid_action(action_key: str) -> bool: return action_key == ALL_CORE_KEY or action_key in ACTION_MAP def _button_meta(action_key: str) -> tuple[str, str]: if action_key == ALL_CORE_KEY: return ALL_CORE_LABEL, ALL_CORE_ICON label, icon, _script, _args = ACTION_MAP[action_key] return label, icon def enqueue_script_job(script_name: str, args: list[str], user: dict, target_label: str) -> int: """Vytvoří run_script job do existující fronty (worker spustí scripts/ s args) a zaaudituje akci (bez hodnot proměnných). Vrací job_id.""" args = list(args or []) payload = {"script_name": script_name} if args: payload["args"] = args payload["arguments"] = args job_id = create_job( job_type="run_script", target_type="maintenance_script", target_id=script_name, payload=payload, user=user, source="portal_runtime", ) log_audit_event( user, action="runtime.action", target_type="runtime", target_id=target_label, metadata={"script_name": script_name, "args": args, "job_id": job_id}, ) return job_id def run_action(action_key: str, user: dict) -> str: """Spustí runtime akci podle klíče. Vrací zprávu pro uživatele.""" if action_key == ALL_CORE_KEY: job_ids = [enqueue_script_job(DEPLOY_CORE_SCRIPT, [arg], user, arg) for _k, _n, _i, arg in CORE_SERVICES] return f"Redeploy všech core služeb byl zařazen do fronty ({len(job_ids)} úloh)." label, _icon, script, args = ACTION_MAP[action_key] job_id = enqueue_script_job(script, args, user, label) return f"Akce {label} byla zařazena do fronty (úloha #{job_id})." def render_action_buttons(keys: list[str], next_url: str) -> str: next_html = html.escape(next_url, quote=True) buttons = "" for key in keys: label, icon = _button_meta(key) buttons += f"""
""" return buttons @router.post("/admin/runtime/action/{action_key}") def runtime_action(action_key: str, next: str = Form(DEFAULT_ACTION_NEXT), user=Depends(require_user)): require_admin(user) if not _is_valid_action(action_key): raise HTTPException(status_code=404, detail="Neznámá akce") message = run_action(action_key, user) redirect_to = next if next.startswith("/portal/admin/") else DEFAULT_ACTION_NEXT return RedirectResponse(url=f"{redirect_to}?message=" + quote(message), status_code=303)