depl
This commit is contained in:
@@ -292,6 +292,29 @@ def get_jobs(
|
|||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_jobs_by_types(job_types, limit: int = 20):
|
||||||
|
run_migrations()
|
||||||
|
job_types = list(job_types)
|
||||||
|
if not job_types:
|
||||||
|
return []
|
||||||
|
|
||||||
|
con = get_connection()
|
||||||
|
placeholders = ",".join("?" for _ in job_types)
|
||||||
|
rows = con.execute(
|
||||||
|
f"""
|
||||||
|
SELECT *
|
||||||
|
FROM jobs
|
||||||
|
WHERE type IN ({placeholders})
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(*job_types, limit),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def count_jobs(
|
def count_jobs(
|
||||||
status: str | None = None,
|
status: str | None = None,
|
||||||
job_type: str | None = None,
|
job_type: str | None = None,
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from .config import read_env_bool, read_env_value
|
from .config import read_env_bool, read_env_value
|
||||||
from .routes import alerting, apps, audit, auth, backups, catalog, deployments, developers, health, incidents, jobs, migration_readiness, operations, scheduled_scripts, users, workers
|
from .routes import alerting, apps, audit, auth, backups, catalog, deployments, developers, health, incidents, jobs, migration_readiness, operations, runtime, scheduled_scripts, users, workers
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
@@ -38,6 +38,7 @@ def create_app() -> FastAPI:
|
|||||||
app.include_router(workers.router)
|
app.include_router(workers.router)
|
||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
app.include_router(audit.router)
|
app.include_router(audit.router)
|
||||||
|
app.include_router(runtime.router)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ PORTAL_SECTIONS = [
|
|||||||
"Pravidla alertů a jejich skripty; úpravy jen pro administrátory.", "developer / admin"),
|
"Pravidla alertů a jejich skripty; úpravy jen pro administrátory.", "developer / admin"),
|
||||||
("fa-gauge-high", "Přehled", "/portal/operations",
|
("fa-gauge-high", "Přehled", "/portal/operations",
|
||||||
"Operační přehled systému, běžící nasazení a stav služeb.", "admin"),
|
"Operační přehled systému, běžící nasazení a stav služeb.", "admin"),
|
||||||
|
("fa-server", "Runtime Management", "/portal/admin/runtime",
|
||||||
|
"Redeploy klíčových komponent (Portal, Tools, Webhook, Worker, Caddy, Gitea, Registry) přes job frontu.", "admin"),
|
||||||
("fa-diagram-project", "Migration Readiness", "/portal/migration-readiness",
|
("fa-diagram-project", "Migration Readiness", "/portal/migration-readiness",
|
||||||
"Připravenost a deploy core služeb AppFactory.", "admin"),
|
"Připravenost a deploy core služeb AppFactory.", "admin"),
|
||||||
("fa-rocket", "Nasazení", "/portal/deployments",
|
("fa-rocket", "Nasazení", "/portal/deployments",
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import html
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
|
from app.auth import require_user
|
||||||
|
from app.db.audit import log_audit_event
|
||||||
|
from app.db.jobs import create_job, get_jobs, get_jobs_by_types
|
||||||
|
from app.routes.jobs import render_job_status
|
||||||
|
from app.templates.layout import page
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
# Klíčové AppFactory komponenty, které lze z Portálu znovu nasadit. Pořadí určuje zobrazení v UI.
|
||||||
|
# key – identifikátor komponenty (target_id jobu i suffix job typu / shell skriptu)
|
||||||
|
# name – zobrazený název
|
||||||
|
# desc – krátký popis
|
||||||
|
# icon – FontAwesome ikona
|
||||||
|
RUNTIME_COMPONENTS = [
|
||||||
|
("portal", "Portal", "Webové administrační rozhraní AppFactory (tento Portál).", "fa-window-maximize"),
|
||||||
|
("tools", "Tools", "Sdílené provozní skripty a nástroje (appfactory-tools).", "fa-screwdriver-wrench"),
|
||||||
|
("webhook", "Webhook", "Příjem a zpracování Gitea webhooků (spouští deploy).", "fa-bolt"),
|
||||||
|
("worker", "Worker", "Centrální worker zpracovávající úlohy z fronty.", "fa-gears"),
|
||||||
|
("caddy", "Caddy", "Reverzní proxy a TLS pro Portál i služby.", "fa-shield-halved"),
|
||||||
|
("gitea", "Gitea", "Git server a registr repozitářů.", "fa-code-branch"),
|
||||||
|
("registry", "Registry", "Docker registr s image jednotlivých služeb.", "fa-box-archive"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mapování komponenty -> typ jobu. Job typy budou později mapovány na maintenance/redeploy-<key>.sh.
|
||||||
|
COMPONENT_JOB_TYPE = {key: f"redeploy-{key}" for key, _name, _desc, _icon in RUNTIME_COMPONENTS}
|
||||||
|
RUNTIME_JOB_TYPES = tuple(COMPONENT_JOB_TYPE.values())
|
||||||
|
COMPONENT_NAMES = {key: name for key, name, _desc, _icon in RUNTIME_COMPONENTS}
|
||||||
|
|
||||||
|
DASHBOARD_LIMIT = 20
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(user: dict) -> None:
|
||||||
|
if (user.get("role") or "").lower() != "admin":
|
||||||
|
raise HTTPException(status_code=403, detail="Runtime Management je dostupný pouze administrátorům")
|
||||||
|
|
||||||
|
|
||||||
|
def _last_action_for(job_type: str) -> str:
|
||||||
|
jobs = get_jobs(job_type=job_type, limit=1)
|
||||||
|
if not jobs:
|
||||||
|
return '<span class="muted">—</span>'
|
||||||
|
job = jobs[0]
|
||||||
|
job_id = html.escape(str(job.get("id", "")))
|
||||||
|
created_at = html.escape(job.get("created_at", "") or "")
|
||||||
|
status = render_job_status(job.get("status"))
|
||||||
|
return f'{status}<br><a href="/portal/jobs/{job_id}"><small>{created_at} · #{job_id}</small></a>'
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/runtime", response_class=HTMLResponse)
|
||||||
|
def runtime_page(request: Request, message: str = "", error: str = "", user=Depends(require_user)):
|
||||||
|
require_admin(user)
|
||||||
|
|
||||||
|
notice = ""
|
||||||
|
if message:
|
||||||
|
notice = f'<p class="alert">{html.escape(message)}</p>'
|
||||||
|
if error:
|
||||||
|
notice = f'<p class="alert alert-danger">{html.escape(error)}</p>'
|
||||||
|
|
||||||
|
component_rows = ""
|
||||||
|
for key, name, desc, icon in RUNTIME_COMPONENTS:
|
||||||
|
last_action = _last_action_for(COMPONENT_JOB_TYPE[key])
|
||||||
|
component_rows += f"""
|
||||||
|
<tr>
|
||||||
|
<td><strong><i class="fa-solid {icon}" aria-hidden="true"></i> {html.escape(name)}</strong></td>
|
||||||
|
<td>{html.escape(desc)}</td>
|
||||||
|
<td>{last_action}</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<form method="post" action="/portal/admin/runtime/redeploy/{key}" class="inline-form"
|
||||||
|
onsubmit="return confirm('Opravdu chcete redeploy komponenty {html.escape(name)}?');">
|
||||||
|
<button type="submit"><i class="fa-solid fa-rotate" aria-hidden="true"></i> Redeploy</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
job_rows = ""
|
||||||
|
for job in get_jobs_by_types(RUNTIME_JOB_TYPES, limit=DASHBOARD_LIMIT):
|
||||||
|
job_id = html.escape(str(job.get("id", "")))
|
||||||
|
created_at = html.escape(job.get("created_at", "") or "")
|
||||||
|
job_type = html.escape(job.get("type", "") or "")
|
||||||
|
status = render_job_status(job.get("status"))
|
||||||
|
result_text = job.get("error_text") or job.get("result_json") or ""
|
||||||
|
result_preview = result_text if len(result_text) <= 160 else f"{result_text[:157]}..."
|
||||||
|
result_cell = f"<code>{html.escape(result_preview)}</code>" if result_preview else '<span class="muted">—</span>'
|
||||||
|
job_rows += f"""
|
||||||
|
<tr>
|
||||||
|
<td>{created_at}</td>
|
||||||
|
<td><a href="/portal/jobs/{job_id}">{job_type}</a></td>
|
||||||
|
<td>{status}</td>
|
||||||
|
<td>{result_cell}</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not job_rows:
|
||||||
|
job_rows = '<tr><td colspan="4">Zatím nebyly spuštěné žádné runtime operace.</td></tr>'
|
||||||
|
|
||||||
|
return page(
|
||||||
|
"Runtime Management",
|
||||||
|
f"""
|
||||||
|
<div class="card">
|
||||||
|
<h2><i class="fa-solid fa-server" aria-hidden="true"></i> Runtime Management</h2>
|
||||||
|
<p class="muted">
|
||||||
|
Centrální správa klíčových AppFactory komponent. Redeploy nespouští akci přímo —
|
||||||
|
vytvoří úlohu do fronty, kterou zpracuje worker. Dostupné pouze administrátorům.
|
||||||
|
</p>
|
||||||
|
{notice}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Komponenty</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Popis</th>
|
||||||
|
<th>Poslední spuštění akce</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
{component_rows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Posledních {DASHBOARD_LIMIT} runtime operací</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Čas</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Výsledek</th>
|
||||||
|
</tr>
|
||||||
|
{job_rows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/runtime/redeploy/{component}")
|
||||||
|
def runtime_redeploy_action(component: str, user=Depends(require_user)):
|
||||||
|
require_admin(user)
|
||||||
|
|
||||||
|
if component not in COMPONENT_JOB_TYPE:
|
||||||
|
raise HTTPException(status_code=404, detail="Neznámá komponenta")
|
||||||
|
|
||||||
|
job_type = COMPONENT_JOB_TYPE[component]
|
||||||
|
name = COMPONENT_NAMES[component]
|
||||||
|
job_id = create_job(
|
||||||
|
job_type=job_type,
|
||||||
|
target_type="runtime",
|
||||||
|
target_id=component,
|
||||||
|
payload={"component": component},
|
||||||
|
user=user,
|
||||||
|
source="portal_runtime",
|
||||||
|
)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="runtime.redeploy",
|
||||||
|
target_type="runtime",
|
||||||
|
target_id=component,
|
||||||
|
metadata={"component": component, "job_type": job_type, "job_id": job_id},
|
||||||
|
)
|
||||||
|
return RedirectResponse(
|
||||||
|
url="/portal/admin/runtime?message=" + quote(f"Redeploy komponenty {name} byl zařazen do fronty (úloha #{job_id})."),
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
@@ -27,6 +27,7 @@ def page(title: str, body: str, user=None) -> str:
|
|||||||
<a href="/portal/deployments"><i class="fa-solid fa-rocket" aria-hidden="true"></i> Nasazení</a>
|
<a href="/portal/deployments"><i class="fa-solid fa-rocket" aria-hidden="true"></i> Nasazení</a>
|
||||||
<a href="/portal/incidents"><i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i> Incidenty</a>
|
<a href="/portal/incidents"><i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i> Incidenty</a>
|
||||||
<a href="/portal/workers"><i class="fa-solid fa-gears" aria-hidden="true"></i> Workery</a>
|
<a href="/portal/workers"><i class="fa-solid fa-gears" aria-hidden="true"></i> Workery</a>
|
||||||
|
<a href="/portal/admin/runtime"><i class="fa-solid fa-server" aria-hidden="true"></i> Runtime Management</a>
|
||||||
<a href="/portal/admin/users"><i class="fa-solid fa-users" aria-hidden="true"></i> Users</a>
|
<a href="/portal/admin/users"><i class="fa-solid fa-users" aria-hidden="true"></i> Users</a>
|
||||||
<a href="/portal/audit"><i class="fa-solid fa-clipboard-list" aria-hidden="true"></i> Audit</a>
|
<a href="/portal/audit"><i class="fa-solid fa-clipboard-list" aria-hidden="true"></i> Audit</a>
|
||||||
<a href="/portal/backups"><i class="fa-solid fa-box-archive" aria-hidden="true"></i> Zálohy</a>
|
<a href="/portal/backups"><i class="fa-solid fa-box-archive" aria-hidden="true"></i> Zálohy</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user