Register apps before queued deployment
This commit is contained in:
+107
@@ -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()
|
||||
|
||||
+107
-27
@@ -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řit službu</button>
|
||||
<span class="form-loader" id="new-app-loader" hidden>
|
||||
<span class="spinner" aria-hidden="true"></span>
|
||||
Vytvářím služ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">← 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,7 @@ 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(
|
||||
app_id,
|
||||
{
|
||||
"name": app_name,
|
||||
"template": selected_template.get("id"),
|
||||
"runtime": selected_template.get("runtime"),
|
||||
"language": selected_template.get("language"),
|
||||
"health_url": selected_template.get("default_health_path"),
|
||||
"container_port": selected_template.get("default_port"),
|
||||
},
|
||||
)
|
||||
deploy_script = clean_optional(selected_template.get("deploy_script")) or DEPLOY_SCRIPT
|
||||
deploy_result = run_command([deploy_script, app_id])
|
||||
|
||||
status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED"
|
||||
if create_result.returncode != 0:
|
||||
log_audit_event(
|
||||
user,
|
||||
action="create_app",
|
||||
@@ -959,23 +956,106 @@ def create_app(
|
||||
"app_id": app_id,
|
||||
"app_name": app_name,
|
||||
"template": selected_template.get("id"),
|
||||
"status": status,
|
||||
"status": "FAILED",
|
||||
"create_returncode": create_result.returncode,
|
||||
"deploy_returncode": deploy_result.returncode,
|
||||
},
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title=f"Vytvoření služby: {status}",
|
||||
title="Vytvoření služby: FAILED",
|
||||
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),
|
||||
],
|
||||
extra_link=f"/apps/{html.escape(app_id)}/docs",
|
||||
extra_label="Otevřít Swagger",
|
||||
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,
|
||||
},
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={
|
||||
"app_id": app_id,
|
||||
"app_name": app_name,
|
||||
"template": selected_template.get("id"),
|
||||
"status": "OK",
|
||||
"create_returncode": create_result.returncode,
|
||||
"job_id": job_id,
|
||||
},
|
||||
)
|
||||
|
||||
return render_result(
|
||||
title="Vytvoření služby: OK",
|
||||
back_url="/portal/apps",
|
||||
sections=[
|
||||
("Výstup vytvoření", create_result.stdout),
|
||||
("Chyba vytvoření", create_result.stderr),
|
||||
("Nasazení", f"Deploy job #{job_id} byl zařazen do fronty."),
|
||||
],
|
||||
extra_link=f"/portal/jobs/{job_id}",
|
||||
extra_label="Otevřít deploy job",
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user