Files
appfactory-portal/app/routes/runtime.py
T
2026-06-17 11:39:52 +02:00

140 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <service>. (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/<script_name> 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"""
<form method="post" action="/portal/admin/runtime/action/{key}" class="inline-form"
onsubmit="return confirm('Spustit akci {html.escape(label)}?');">
<input type="hidden" name="next" value="{next_html}">
<button type="submit"><i class="fa-solid {icon}" aria-hidden="true"></i> {html.escape(label)}</button>
</form>
"""
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)