icons, main .env
This commit is contained in:
@@ -1,18 +1,11 @@
|
||||
import html
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from app.auth import require_user
|
||||
from app.config import (
|
||||
DEFAULT_GITEA_ORG,
|
||||
get_gitea_admin_token,
|
||||
get_gitea_server_url,
|
||||
read_env_value,
|
||||
)
|
||||
from app.db.audit import log_audit_event
|
||||
from app.db.jobs import create_job
|
||||
from app.db.scheduled_scripts import (
|
||||
@@ -25,13 +18,10 @@ from app.db.scheduled_scripts import (
|
||||
)
|
||||
from app.routes.deployments import render_status_pill
|
||||
from app.templates.layout import page
|
||||
from app.tools_repo import TOOLS_REPO_DIR, commit_and_push
|
||||
|
||||
router = APIRouter()
|
||||
TOOLS_REPO_DIR = Path("/opt/appfactory/workspace/appfactory-tools")
|
||||
TOOLS_REPO_NAME = "appfactory-tools"
|
||||
MAINTENANCE_DIR = TOOLS_REPO_DIR / "maintenance"
|
||||
GIT_AUTHOR_NAME = "AppFactory Portal"
|
||||
GIT_AUTHOR_EMAIL = "portal@appfactory.local"
|
||||
MAX_SCRIPT_BYTES = 100 * 1024
|
||||
DEFAULT_SCRIPT_CONTENT = """#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -153,71 +143,6 @@ def save_script_file(script_name: str, content: str) -> None:
|
||||
raise HTTPException(status_code=500, detail="Soubor se nepodařilo uložit")
|
||||
|
||||
|
||||
def _git(args: list[str]):
|
||||
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
||||
try:
|
||||
return subprocess.run(
|
||||
["git", "-C", str(TOOLS_REPO_DIR), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=30,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
raise HTTPException(status_code=500, detail="Git příkaz selhal nebo vypršel limit")
|
||||
|
||||
|
||||
def _git_actor(user: dict) -> str:
|
||||
return (user.get("email") or user.get("display_name") or user.get("username") or "neznámý").strip()
|
||||
|
||||
|
||||
def _gitea_push_remote() -> tuple[str, str | None]:
|
||||
"""Vrátí (remote, token). Remote je autentizovaná gitea URL z tokenů v proměnných,
|
||||
při absenci tokenu fallback na origin. Token vracíme zvlášť, aby šel zamaskovat v chybách."""
|
||||
token = get_gitea_admin_token()
|
||||
server = get_gitea_server_url()
|
||||
if not token or "://" not in server:
|
||||
return "origin", None
|
||||
scheme, rest = server.split("://", 1)
|
||||
org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
|
||||
return f"{scheme}://{token}@{rest}/{org}/{TOOLS_REPO_NAME}.git", token
|
||||
|
||||
|
||||
def _git_error_detail(result, token: str | None = None) -> str:
|
||||
"""Zkombinuje git stderr/stdout do čitelného důvodu chyby (s maskováním tokenu)."""
|
||||
detail = (result.stderr or "").strip() or (result.stdout or "").strip()
|
||||
if token and detail:
|
||||
detail = detail.replace(token, "***")
|
||||
return html.escape(detail) if detail else "git nevrátil žádný výstup"
|
||||
|
||||
|
||||
def commit_and_push(script_name: str, message: str, user: dict) -> None:
|
||||
"""Zacommituje a pushne změnu jednoho maintenance skriptu do gitea (appfactory-tools)."""
|
||||
rel_path = f"maintenance/{script_name}"
|
||||
|
||||
add = _git(["add", "--", rel_path])
|
||||
if add.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Git add selhal (v {TOOLS_REPO_DIR}): {_git_error_detail(add)}")
|
||||
|
||||
# Žádná změna oproti HEAD -> přeskočíme, ať nevznikají prázdné commity.
|
||||
if _git(["diff", "--cached", "--quiet", "--", rel_path]).returncode == 0:
|
||||
return
|
||||
|
||||
commit = _git([
|
||||
"-c", f"user.name={GIT_AUTHOR_NAME}",
|
||||
"-c", f"user.email={GIT_AUTHOR_EMAIL}",
|
||||
"commit", "-m", f"{message} (portál: {_git_actor(user)})",
|
||||
])
|
||||
if commit.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Git commit selhal (v {TOOLS_REPO_DIR}): {_git_error_detail(commit)}")
|
||||
|
||||
branch = (_git(["rev-parse", "--abbrev-ref", "HEAD"]).stdout or "").strip() or "main"
|
||||
remote, token = _gitea_push_remote()
|
||||
push = _git(["push", remote, f"HEAD:{branch}"])
|
||||
if push.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Git push selhal: {_git_error_detail(push, token)}")
|
||||
|
||||
|
||||
def validate_schedule_type(value: str) -> str:
|
||||
schedule_type = clean_optional(value)
|
||||
if schedule_type not in SCHEDULE_TYPES:
|
||||
@@ -410,7 +335,7 @@ def scheduled_scripts_page(request: Request, user=Depends(require_user)):
|
||||
"Plánované skripty",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Plánované skripty</h2>
|
||||
<h2><i class="fa-solid fa-calendar-days" aria-hidden="true"></i> Plánované skripty</h2>
|
||||
<p class="muted">Přehled skriptů spouštěných schedulerem nebo ručně z portálu.</p>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts/new">+ Nový plánovaný skript</a></p>
|
||||
</div>
|
||||
@@ -444,7 +369,7 @@ def new_scheduled_script_form(request: Request, user=Depends(require_user)):
|
||||
"Nový plánovaný skript",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Nový plánovaný skript</h2>
|
||||
<h2><i class="fa-solid fa-circle-plus" aria-hidden="true"></i> Nový plánovaný skript</h2>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts">← Zpět</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -472,7 +397,7 @@ def create_scheduled_script_action(
|
||||
created_name = metadata["script_name"]
|
||||
if not script_path(created_name).exists():
|
||||
save_script_file(created_name, DEFAULT_SCRIPT_CONTENT)
|
||||
commit_and_push(created_name, f"Vytvořen skript {created_name}", user)
|
||||
commit_and_push(f"maintenance/{created_name}", f"Vytvořen skript {created_name}", user)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="scheduled_script.created",
|
||||
@@ -510,7 +435,7 @@ def scheduled_script_detail(script_id: int, request: Request, user=Depends(requi
|
||||
html.escape(script.get("name", "") or "Plánovaný skript"),
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>{html.escape(script.get("name", "") or "")}</h2>
|
||||
<h2><i class="fa-solid fa-calendar-day" aria-hidden="true"></i> {html.escape(script.get("name", "") or "")}</h2>
|
||||
<p>
|
||||
<a class="btn" href="/portal/scheduled-scripts">← Zpět na plánované skripty</a>
|
||||
<a class="btn btn-secondary" href="/portal/scheduled-scripts/{script_id}/edit">Upravit</a>
|
||||
@@ -560,7 +485,7 @@ def edit_scheduled_script_form(script_id: int, request: Request, user=Depends(re
|
||||
"Upravit plánovaný skript",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Upravit plánovaný skript</h2>
|
||||
<h2><i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Upravit plánovaný skript</h2>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts/{script_id}">← Zpět</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -637,7 +562,7 @@ def update_scheduled_script_file(
|
||||
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||
script_name = validate_script_name(script.get("script_name", "") or "")
|
||||
save_script_file(script_name, content)
|
||||
commit_and_push(script_name, f"Úprava skriptu {script_name}", user)
|
||||
commit_and_push(f"maintenance/{script_name}", f"Úprava skriptu {script_name}", user)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="scheduled_script.file_updated",
|
||||
@@ -682,7 +607,7 @@ def delete_scheduled_script_action(script_id: int, user=Depends(require_user)):
|
||||
path = script_path(script_name)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
commit_and_push(script_name, f"Smazán skript {script_name}", user)
|
||||
commit_and_push(f"maintenance/{script_name}", f"Smazán skript {script_name}", user)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="scheduled_script.deleted",
|
||||
|
||||
Reference in New Issue
Block a user