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()
|
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):
|
def update_app_metadata(app_id: str, metadata: dict):
|
||||||
run_migrations()
|
run_migrations()
|
||||||
con = get_connection()
|
con = get_connection()
|
||||||
|
|||||||
+95
-15
@@ -25,7 +25,7 @@ from ..db.apps import (
|
|||||||
get_apps,
|
get_apps,
|
||||||
update_app_metadata,
|
update_app_metadata,
|
||||||
update_app_resources,
|
update_app_resources,
|
||||||
update_app_template_metadata,
|
upsert_created_app,
|
||||||
update_app_variable,
|
update_app_variable,
|
||||||
)
|
)
|
||||||
from ..db.audit import log_audit_event
|
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>
|
<h2>Vytvořit novou službu</h2>
|
||||||
<p class="muted">Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.</p>
|
<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>
|
<p>
|
||||||
<label>ID služby</label><br>
|
<label>ID služby</label><br>
|
||||||
<input name="app_id" placeholder="gmail-service" required>
|
<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>
|
<select name="template" required>{template_options}</select>
|
||||||
</p>
|
</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>
|
</form>
|
||||||
|
<div id="new-app-result"></div>
|
||||||
|
|
||||||
<p><a href="/portal/apps">← Zpět</a></p>
|
<p><a href="/portal/apps">← Zpět</a></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -924,6 +932,10 @@ def create_app(
|
|||||||
app_id: str = Form(...),
|
app_id: str = Form(...),
|
||||||
app_name: str = Form(...),
|
app_name: str = Form(...),
|
||||||
template: str = Form(...),
|
template: str = Form(...),
|
||||||
|
description: str = Form(""),
|
||||||
|
owner: str = Form(""),
|
||||||
|
memory: str = Form(""),
|
||||||
|
cpus: str = Form(""),
|
||||||
user=Depends(require_user),
|
user=Depends(require_user),
|
||||||
):
|
):
|
||||||
selected_template = get_app_template(template, create_enabled=True)
|
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)
|
return HTMLResponse("Šablona nemá nastavený create script", status_code=400)
|
||||||
|
|
||||||
create_result = run_command([create_script, app_id, app_name])
|
create_result = run_command([create_script, app_id, app_name])
|
||||||
if create_result.returncode == 0:
|
if create_result.returncode != 0:
|
||||||
update_app_template_metadata(
|
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,
|
app_id,
|
||||||
{
|
{
|
||||||
"name": app_name,
|
"name": app_name,
|
||||||
"template": selected_template.get("id"),
|
"template": selected_template.get("id"),
|
||||||
"runtime": selected_template.get("runtime"),
|
"runtime": selected_template.get("runtime"),
|
||||||
"language": selected_template.get("language"),
|
"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"),
|
"health_url": selected_template.get("default_health_path"),
|
||||||
"container_port": selected_template.get("default_port"),
|
"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
|
except Exception as exc:
|
||||||
deploy_result = run_command([deploy_script, app_id])
|
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(
|
log_audit_event(
|
||||||
user,
|
user,
|
||||||
action="create_app",
|
action="create_app",
|
||||||
@@ -959,23 +1040,22 @@ def create_app(
|
|||||||
"app_id": app_id,
|
"app_id": app_id,
|
||||||
"app_name": app_name,
|
"app_name": app_name,
|
||||||
"template": selected_template.get("id"),
|
"template": selected_template.get("id"),
|
||||||
"status": status,
|
"status": "OK",
|
||||||
"create_returncode": create_result.returncode,
|
"create_returncode": create_result.returncode,
|
||||||
"deploy_returncode": deploy_result.returncode,
|
"job_id": job_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return render_result(
|
return render_result(
|
||||||
title=f"Vytvoření služby: {status}",
|
title="Vytvoření služby: OK",
|
||||||
back_url="/portal/apps",
|
back_url="/portal/apps",
|
||||||
sections=[
|
sections=[
|
||||||
("Výstup vytvoření", create_result.stdout),
|
("Výstup vytvoření", create_result.stdout),
|
||||||
("Chyba vytvoření", create_result.stderr),
|
("Chyba vytvoření", create_result.stderr),
|
||||||
("Výstup nasazení", deploy_result.stdout),
|
("Nasazení", f"Deploy job #{job_id} byl zařazen do fronty."),
|
||||||
("Chyba nasazení", deploy_result.stderr),
|
|
||||||
],
|
],
|
||||||
extra_link=f"/apps/{html.escape(app_id)}/docs",
|
extra_link=f"/portal/jobs/{job_id}",
|
||||||
extra_label="Otevřít Swagger",
|
extra_label="Otevřít deploy job",
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -117,3 +117,85 @@ document.addEventListener("click", (event) => {
|
|||||||
modal.close();
|
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;
|
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 {
|
.detail-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user