Files
appfactory-portal/app/routes/apps.py
T
2026-05-28 11:49:59 +02:00

311 lines
11 KiB
Python

import html
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse
from ..auth import require_user
from ..config import (
DEFAULT_APPFACTORY_HOST,
DEFAULT_GITEA_ORG,
DELETE_APP_SCRIPT,
DEPLOY_SCRIPT,
GENERATE_COMPOSE_SCRIPT,
NEW_APP_SCRIPT,
read_env_value,
)
from ..db.apps import get_apps, update_app_resources
from ..db.audit import log_audit_event
from ..shell import run_command
from ..templates.layout import page, render_result
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
def index(request: Request, user=Depends(require_user)):
apps = get_apps()
gitea_url = read_env_value("GITEA_URL", "")
gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
host = read_env_value("APPFACTORY_HOST", DEFAULT_APPFACTORY_HOST)
rows = ""
for item in apps:
app_id = html.escape(item.get("id", ""))
status = html.escape(item.get("status", ""))
docs = html.escape(item.get("docs", f"/apps/{app_id}/docs"))
memory = item.get("memory", "")
cpus = item.get("cpus", "")
http_clone = html.escape(f"git clone {gitea_url}/{gitea_org}/{app_id}.git")
ssh_clone = html.escape(f"git clone ssh://git@{host}:2222/{gitea_org}/{app_id}.git")
memory_options = ""
for value, label in [
("", "Výchozí"),
("256m", "256 MB - malá služba"),
("512m", "512 MB - běžná služba"),
("1g", "1 GB - větší služba"),
("2g", "2 GB - náročná služba"),
]:
selected = "selected" if memory == value else ""
memory_options += f'<option value="{value}" {selected}>{label}</option>'
cpu_options = ""
for value, label in [
("", "Výchozí"),
("0.25", "0,25 CPU - velmi malá služba"),
("0.50", "0,50 CPU - běžná služba"),
("1.00", "1 CPU - celé jádro"),
("2.00", "2 CPU - náročná služba"),
]:
selected = "selected" if cpus == value else ""
cpu_options += f'<option value="{value}" {selected}>{label}</option>'
rows += f"""
<tr>
<td>
<strong>{app_id}</strong><br>
<span class="muted">/apps/{app_id}</span>
</td>
<td><span class="pill">{status}</span></td>
<td><a href="{docs}">Swagger</a></td>
<td>
<form method="post" action="/portal/update-resources">
<input type="hidden" name="app_id" value="{app_id}">
<div class="inline-form">
<select name="memory">{memory_options}</select>
<select name="cpus">{cpu_options}</select>
<button type="submit">Použít</button>
</div>
<div class="resource-help">
Paměť je limit RAM. CPU určuje maximální podíl výpočetního výkonu.
Příklad: 0,50 = polovina jádra, 1,00 = celé jádro.
</div>
</form>
</td>
<td>
<details>
<summary>Příkazy pro klonování</summary>
<label class="muted">HTTP</label>
<div class="cmd-row">
<input readonly value="{http_clone}" id="http-{app_id}">
<button onclick="copyValue('http-{app_id}')">Kopírovat</button>
</div>
<label class="muted">SSH</label>
<div class="cmd-row">
<input readonly value="{ssh_clone}" id="ssh-{app_id}">
<button onclick="copyValue('ssh-{app_id}')">Kopírovat</button>
</div>
</details>
</td>
<td>
<form method="post" action="/portal/delete-app" onsubmit="return confirm('Smazat {app_id}? Tím se odstraní kontejner, image, workspace, záznam v katalogu a Gitea repozitář.');">
<input type="hidden" name="app_id" value="{app_id}">
<button type="submit" class="danger">Smazat</button>
</form>
</td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="6">Zatím nejsou nasazené žádné aplikace.</td></tr>'
return page(
"Aplikace",
f"""
<div class="grid">
<div class="card">
<h2>Aplikace</h2>
<p class="muted">Vytváření, nasazení, klonování, nastavení prostředků a mazání služeb.</p>
<a class="btn" href="/portal/new-app">+ Nová aplikace</a>
</div>
<div class="card">
<h2>Zálohy</h2>
<p class="muted">Vytváření záloh a kopírování příkazů pro obnovu. Obnova je záměrně ruční a chráněná.</p>
<a class="btn" href="/portal/backups">Spravovat zálohy</a>
</div>
<div class="card">
<h2>Nasazení</h2>
<p class="muted">Historie posledních běhů nasazení, stavů a výstupů z deploy procesu.</p>
<a class="btn" href="/portal/deployments">Zobrazit nasazení</a>
</div>
</div>
<div class="card">
<h2>Nasazené aplikace</h2>
<table>
<tr>
<th>Aplikace</th>
<th>Status</th>
<th>Dokumentace</th>
<th>Prostředky</th>
<th>Git</th>
<th>Akce</th>
</tr>
{rows}
</table>
</div>
""",
user=user,
)
@router.get("/new-app", response_class=HTMLResponse)
def new_app_form(request: Request, user=Depends(require_user)):
return page(
"Nová aplikace",
"""
<div class="card">
<h2>Vytvořit novou aplikaci</h2>
<p class="muted">Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.</p>
<form method="post" action="/portal/new-app">
<p>
<label>ID aplikace</label><br>
<input name="app_id" placeholder="gmail-service" required>
</p>
<p>
<label>Název aplikace</label><br>
<input name="app_name" placeholder="Gmail služba" required>
</p>
<p>
<label>Šablona</label><br>
<select name="template">
<option value="python-fastapi">Python FastAPI</option>
</select>
</p>
<button type="submit">Vytvořit aplikaci</button>
</form>
<p><a href="/portal">&larr; Zpět</a></p>
</div>
""",
user=user,
)
@router.post("/new-app", response_class=HTMLResponse)
def create_app(
app_id: str = Form(...),
app_name: str = Form(...),
template: str = Form(...),
user=Depends(require_user),
):
if template != "python-fastapi":
return HTMLResponse("Nepodporovaná šablona", status_code=400)
create_result = run_command([NEW_APP_SCRIPT, app_id, app_name])
deploy_result = run_command([DEPLOY_SCRIPT, app_id])
status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED"
log_audit_event(
user,
action="create_app",
target_type="app",
target_id=app_id,
metadata={
"app_id": app_id,
"app_name": app_name,
"template": template,
"status": status,
"create_returncode": create_result.returncode,
"deploy_returncode": deploy_result.returncode,
},
)
return render_result(
title=f"Vytvoření aplikace: {status}",
back_url="/portal",
sections=[
("Výstup vytvoření", create_result.stdout),
("Chyba vytvoření", create_result.stderr),
("Výstup nasazení", deploy_result.stdout),
("Chyba nasazení", deploy_result.stderr),
],
extra_link=f"/apps/{html.escape(app_id)}/docs",
extra_label="Otevřít Swagger",
user=user,
)
@router.post("/delete-app", response_class=HTMLResponse)
def delete_app(app_id: str = Form(...), user=Depends(require_user)):
result = run_command([DELETE_APP_SCRIPT, app_id])
status = "OK" if result.returncode == 0 else "FAILED"
log_audit_event(
user,
action="delete_app",
target_type="app",
target_id=app_id,
metadata={
"app_id": app_id,
"status": status,
"returncode": result.returncode,
},
)
return render_result(
title=f"Smazání aplikace: {status}",
back_url="/portal",
sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
user=user,
)
@router.post("/update-resources", response_class=HTMLResponse)
def update_resources(
app_id: str = Form(...),
memory: str = Form(""),
cpus: str = Form(""),
user=Depends(require_user),
):
memory = memory.strip()
cpus = cpus.strip()
update_app_resources(app_id, memory, cpus)
catalog_result = run_command(["/tools/generate-catalog.sh"])
compose_result = run_command([GENERATE_COMPOSE_SCRIPT])
deploy_result = run_command([DEPLOY_SCRIPT, app_id])
status = (
"OK"
if catalog_result.returncode == 0 and compose_result.returncode == 0 and deploy_result.returncode == 0
else "FAILED"
)
log_audit_event(
user,
action="update_resources",
target_type="app",
target_id=app_id,
metadata={
"app_id": app_id,
"memory": memory,
"cpus": cpus,
"status": status,
"catalog_returncode": catalog_result.returncode,
"compose_returncode": compose_result.returncode,
"deploy_returncode": deploy_result.returncode,
},
)
return render_result(
title=f"Úprava prostředků: {status}",
back_url="/portal",
sections=[
("Výstup katalogu", catalog_result.stdout),
("Chyba katalogu", catalog_result.stderr),
("Výstup compose", compose_result.stdout),
("Chyba compose", compose_result.stderr),
("Výstup nasazení", deploy_result.stdout),
("Chyba nasazení", deploy_result.stderr),
],
user=user,
)