141 lines
5.8 KiB
Python
141 lines
5.8 KiB
Python
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").
|
||
# POZOR: předáváme HOLÝ název skriptu bez cesty – adresář scripts/ si doplní Worker sám. Cokoli
|
||
# s cestou ("scripts/...") Worker odmítne hláškou "Invalid script name" (viz config.py a
|
||
# is_allowed_worker_script). Stejné pravidlo hlídá i allowlist ALLOWED_WORKER_SCRIPTS.
|
||
DEPLOY_CORE_SCRIPT = "deploy-core-service.sh"
|
||
CADDY_SCRIPT = "generate-caddyfile.sh"
|
||
ENABLED_APPS_SCRIPT = "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)
|