Register apps before queued deployment

This commit is contained in:
JiriUhlir
2026-06-03 10:38:23 +02:00
parent 0a37507d86
commit 46f8024d4e
4 changed files with 334 additions and 15 deletions
+107
View File
@@ -174,6 +174,113 @@ def update_app_template_metadata(app_id: str, metadata: dict):
con.close()
def upsert_created_app(app_id: str, metadata: dict):
run_migrations()
con = get_connection()
values = (
metadata.get("name") or app_id,
metadata.get("language") or None,
metadata.get("version") or "1.0.0",
metadata.get("status") or "created",
metadata.get("memory") or None,
metadata.get("cpus") or None,
metadata.get("description") or None,
metadata.get("owner") or None,
metadata.get("template") or None,
metadata.get("runtime") or None,
metadata.get("repository_url") or None,
metadata.get("repository_name") or app_id,
metadata.get("default_branch") or "main",
metadata.get("health_url") or None,
metadata.get("container_port"),
1 if metadata.get("is_public") else 0,
1 if metadata.get("is_enabled") else 0,
)
try:
cur = con.execute(
"""
UPDATE apps
SET name = ?,
language = ?,
version = ?,
status = ?,
memory = ?,
cpus = ?,
description = ?,
owner = ?,
template = ?,
runtime = ?,
repository_url = ?,
repository_name = ?,
default_branch = ?,
health_url = ?,
container_port = ?,
is_public = ?,
is_enabled = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(*values, app_id),
)
if cur.rowcount == 0:
con.execute(
"""
INSERT INTO apps (
id,
name,
language,
version,
status,
memory,
cpus,
updated_at,
description,
owner,
template,
runtime,
repository_url,
repository_name,
default_branch,
health_url,
container_port,
is_public,
is_enabled
)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
app_id,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5],
values[6],
values[7],
values[8],
values[9],
values[10],
values[11],
values[12],
values[13],
values[14],
values[15],
values[16],
),
)
con.commit()
except Exception:
con.rollback()
raise
finally:
con.close()
def update_app_metadata(app_id: str, metadata: dict):
run_migrations()
con = get_connection()
+95 -15
View File
@@ -25,7 +25,7 @@ from ..db.apps import (
get_apps,
update_app_metadata,
update_app_resources,
update_app_template_metadata,
upsert_created_app,
update_app_variable,
)
from ..db.audit import log_audit_event
@@ -893,7 +893,7 @@ def new_app_form(request: Request, user=Depends(require_user)):
<h2>Vytvořit novou službu</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">
<form method="post" action="/portal/new-app" id="new-app-form" data-ajax-form="new-app">
<p>
<label>ID služby</label><br>
<input name="app_id" placeholder="gmail-service" required>
@@ -909,8 +909,16 @@ def new_app_form(request: Request, user=Depends(require_user)):
<select name="template" required>{template_options}</select>
</p>
<button type="submit">Vytvořit službu</button>
<div class="form-actions">
<button type="submit">Vytvo&rcaron;it slu&zcaron;bu</button>
<span class="form-loader" id="new-app-loader" hidden>
<span class="spinner" aria-hidden="true"></span>
Vytv&aacute;&rcaron;&iacute;m slu&zcaron;bu...
</span>
</div>
<div class="async-status" id="new-app-status" aria-live="polite"></div>
</form>
<div id="new-app-result"></div>
<p><a href="/portal/apps">&larr; Zpět</a></p>
</div>
@@ -924,6 +932,10 @@ def create_app(
app_id: str = Form(...),
app_name: str = Form(...),
template: str = Form(...),
description: str = Form(""),
owner: str = Form(""),
memory: str = Form(""),
cpus: str = Form(""),
user=Depends(require_user),
):
selected_template = get_app_template(template, create_enabled=True)
@@ -934,22 +946,91 @@ def create_app(
return HTMLResponse("Šablona nemá nastavený create script", status_code=400)
create_result = run_command([create_script, app_id, app_name])
if create_result.returncode == 0:
update_app_template_metadata(
if create_result.returncode != 0:
log_audit_event(
user,
action="create_app",
target_type="app",
target_id=app_id,
metadata={
"app_id": app_id,
"app_name": app_name,
"template": selected_template.get("id"),
"status": "FAILED",
"create_returncode": create_result.returncode,
},
)
return render_result(
title="Vytvoření služby: FAILED",
back_url="/portal/apps",
sections=[
("Výstup vytvoření", create_result.stdout),
("Chyba vytvoření", create_result.stderr),
],
user=user,
)
gitea_url = read_env_value("GITEA_URL", "").rstrip("/")
gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
repository_url = f"{gitea_url}/{gitea_org}/{app_id}.git" if gitea_url else None
owner_value = clean_optional(owner) or user.get("display_name") or user.get("username") or None
try:
upsert_created_app(
app_id,
{
"name": app_name,
"template": selected_template.get("id"),
"runtime": selected_template.get("runtime"),
"language": selected_template.get("language"),
"version": "1.0.0",
"status": "created",
"memory": clean_optional(memory),
"cpus": clean_optional(cpus),
"description": clean_optional(description),
"owner": owner_value,
"repository_name": app_id,
"repository_url": repository_url,
"default_branch": "main",
"health_url": selected_template.get("default_health_path"),
"container_port": selected_template.get("default_port"),
"is_public": True,
"is_enabled": True,
},
)
deploy_script = clean_optional(selected_template.get("deploy_script")) or DEPLOY_SCRIPT
deploy_result = run_command([deploy_script, app_id])
except Exception as exc:
log_audit_event(
user,
action="create_app",
target_type="app",
target_id=app_id,
metadata={
"app_id": app_id,
"app_name": app_name,
"template": selected_template.get("id"),
"status": "FAILED",
"create_returncode": create_result.returncode,
"registration_error": str(exc),
},
)
return render_result(
title="Registrace služby: FAILED",
back_url="/portal/apps",
sections=[
("Výstup vytvoření", create_result.stdout),
("Chyba vytvoření", create_result.stderr),
("Chyba registrace", str(exc)),
],
user=user,
)
status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED"
job_id = create_job(
job_type="deploy_app",
target_type="app",
target_id=app_id,
payload={"template": selected_template.get("id")},
user=user,
source="portal",
)
log_audit_event(
user,
action="create_app",
@@ -959,23 +1040,22 @@ def create_app(
"app_id": app_id,
"app_name": app_name,
"template": selected_template.get("id"),
"status": status,
"status": "OK",
"create_returncode": create_result.returncode,
"deploy_returncode": deploy_result.returncode,
"job_id": job_id,
},
)
return render_result(
title=f"Vytvoření služby: {status}",
title="Vytvoření služby: OK",
back_url="/portal/apps",
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),
("Nasazení", f"Deploy job #{job_id} byl zařazen do fronty."),
],
extra_link=f"/apps/{html.escape(app_id)}/docs",
extra_label="Otevřít Swagger",
extra_link=f"/portal/jobs/{job_id}",
extra_label="Otevřít deploy job",
user=user,
)
+82
View File
@@ -117,3 +117,85 @@ document.addEventListener("click", (event) => {
modal.close();
}
});
function setFormDisabled(form, disabled) {
for (const field of form.querySelectorAll("input, select, textarea, button")) {
field.disabled = disabled;
}
}
function extractMainHtml(responseText) {
const parser = new DOMParser();
const doc = parser.parseFromString(responseText, "text/html");
const main = doc.querySelector("main");
return main ? main.innerHTML : "";
}
document.addEventListener("DOMContentLoaded", () => {
const form = document.querySelector('[data-ajax-form="new-app"]');
if (!form) {
return;
}
const loader = document.getElementById("new-app-loader");
const status = document.getElementById("new-app-status");
const result = document.getElementById("new-app-result");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(form);
if (status) {
status.className = "async-status";
status.textContent = "Vytv\u00e1\u0159\u00edm slu\u017ebu, neobnovujte str\u00e1nku.";
}
if (result) {
result.innerHTML = "";
}
if (loader) {
loader.hidden = false;
}
setFormDisabled(form, true);
try {
const response = await fetch(form.action, {
method: "POST",
body: formData,
headers: {
"X-Requested-With": "fetch",
},
});
const responseText = await response.text();
const mainHtml = extractMainHtml(responseText);
if (mainHtml) {
const main = document.querySelector("main");
if (main) {
main.innerHTML = mainHtml;
}
return;
}
if (!response.ok) {
throw new Error(responseText || "Vytvo\u0159en\u00ed slu\u017eby selhalo.");
}
if (result) {
result.innerHTML = responseText;
}
if (status) {
status.textContent = "";
}
} catch (error) {
if (status) {
status.className = "async-status async-status-error";
status.textContent = error.message || "Vytvo\u0159en\u00ed slu\u017eby selhalo.";
}
setFormDisabled(form, false);
} finally {
if (loader) {
loader.hidden = true;
}
}
});
});
+50
View File
@@ -489,6 +489,56 @@ textarea {
grid-column: 2;
}
button:disabled,
input:disabled,
select:disabled,
textarea:disabled {
cursor: wait;
opacity: 0.72;
}
.form-loader {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-weight: 700;
}
.form-loader[hidden] {
display: none;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.async-status {
grid-column: 2;
min-height: 22px;
color: var(--muted);
font-weight: 700;
}
.async-status-error {
color: var(--danger);
}
#new-app-result {
margin-top: 18px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.detail-tabs {
display: flex;
flex-wrap: wrap;