Files
appfactory-portal/app/db/apps.py
T
2026-07-30 07:13:12 +02:00

731 lines
19 KiB
Python

from app.db.database import get_connection
from app.db.migrations import run_migrations
SUCCESS_STATUSES = ("ok", "success", "succeeded", "done", "deployed", "completed")
FAILED_STATUSES = ("failed", "failure", "error", "cancelled", "canceled")
RUNNING_STATUSES = ("running", "pending", "queued", "in_progress", "starting")
def get_apps():
run_migrations()
con = get_connection()
rows = con.execute(
"""
SELECT
a.id,
a.name,
a.language,
a.version,
a.status,
COALESCE(a.memory, '') AS memory,
COALESCE(a.cpus, '') AS cpus,
a.updated_at,
(
SELECT d.status
FROM deployments d
WHERE d.app_id = a.id
ORDER BY d.id DESC
LIMIT 1
) AS last_deploy_status,
(
SELECT d.started_at
FROM deployments d
WHERE d.app_id = a.id
ORDER BY d.id DESC
LIMIT 1
) AS last_deploy_started_at
FROM apps a
ORDER BY a.id
"""
).fetchall()
con.close()
return [dict(row) for row in rows]
def get_app(app_id: str):
run_migrations()
con = get_connection()
row = con.execute(
"""
SELECT
id,
name,
language,
version,
status,
memory,
cpus,
updated_at,
description,
owner,
template,
runtime,
repository_url,
repository_name,
default_branch,
domain,
health_url,
container_port,
COALESCE(is_public, 0) AS is_public,
COALESCE(is_enabled, 1) AS is_enabled
FROM apps
WHERE id = ?
""",
(app_id,),
).fetchone()
con.close()
return dict(row) if row else None
def get_app_templates(create_enabled: bool = False):
run_migrations()
con = get_connection()
create_filter = "AND create_enabled = 1" if create_enabled else ""
rows = con.execute(
f"""
SELECT
id,
name,
runtime,
language,
description,
default_port,
default_health_path,
create_script,
deploy_script,
COALESCE(is_enabled, 1) AS is_enabled,
COALESCE(create_enabled, 0) AS create_enabled
FROM app_templates
WHERE COALESCE(is_enabled, 1) = 1
{create_filter}
ORDER BY name
"""
).fetchall()
con.close()
return [dict(row) for row in rows]
def get_app_template(template_id: str, create_enabled: bool = False):
run_migrations()
con = get_connection()
create_filter = "AND COALESCE(create_enabled, 0) = 1" if create_enabled else ""
row = con.execute(
f"""
SELECT
id,
name,
runtime,
language,
description,
default_port,
default_health_path,
create_script,
deploy_script,
COALESCE(is_enabled, 1) AS is_enabled,
COALESCE(create_enabled, 0) AS create_enabled
FROM app_templates
WHERE id = ?
AND COALESCE(is_enabled, 1) = 1
{create_filter}
""",
(template_id,),
).fetchone()
con.close()
return dict(row) if row else None
def update_app_template_metadata(app_id: str, metadata: dict):
run_migrations()
con = get_connection()
con.execute(
"""
UPDATE apps
SET name = ?,
template = ?,
runtime = ?,
language = ?,
health_url = ?,
container_port = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(
metadata.get("name") or None,
metadata.get("template") or None,
metadata.get("runtime") or None,
metadata.get("language") or None,
metadata.get("health_url") or None,
metadata.get("container_port"),
app_id,
),
)
con.commit()
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()
con.execute(
"""
UPDATE apps
SET name = ?,
description = ?,
owner = ?,
template = ?,
runtime = ?,
repository_url = ?,
repository_name = ?,
default_branch = ?,
domain = ?,
health_url = ?,
container_port = ?,
is_public = ?,
is_enabled = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(
metadata.get("name") 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 None,
metadata.get("default_branch") or None,
metadata.get("domain") or None,
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,
app_id,
),
)
con.commit()
con.close()
def get_app_variables(app_id: str):
run_migrations()
con = get_connection()
rows = con.execute(
"""
SELECT id, app_id, "key", value, COALESCE(is_secret, 0) AS is_secret
FROM app_variables
WHERE app_id = ?
ORDER BY "key"
""",
(app_id,),
).fetchall()
con.close()
return [dict(row) for row in rows]
def create_app_variable(app_id: str, key: str, value: str, is_secret: bool):
run_migrations()
con = get_connection()
con.execute(
"""
INSERT INTO app_variables (app_id, "key", value, is_secret, created_at, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""",
(app_id, key, value, 1 if is_secret else 0),
)
con.commit()
con.close()
def update_app_variable(variable_id: int, app_id: str, key: str, value: str | None, is_secret: bool):
run_migrations()
con = get_connection()
if value is None:
con.execute(
"""
UPDATE app_variables
SET "key" = ?,
is_secret = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND app_id = ?
""",
(key, 1 if is_secret else 0, variable_id, app_id),
)
else:
con.execute(
"""
UPDATE app_variables
SET "key" = ?,
value = ?,
is_secret = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND app_id = ?
""",
(key, value, 1 if is_secret else 0, variable_id, app_id),
)
con.commit()
con.close()
def delete_app_variable(variable_id: int, app_id: str):
run_migrations()
con = get_connection()
con.execute(
"""
DELETE FROM app_variables
WHERE id = ? AND app_id = ?
""",
(variable_id, app_id),
)
con.commit()
con.close()
def get_app_ip_access_rules(app_id: str):
run_migrations()
con = get_connection()
rows = con.execute(
"""
SELECT r.id, r.app_id, r.ip_cidr, r.methods, r.description,
COALESCE(r.is_enabled, 1) AS is_enabled, r.created_at, r.updated_at,
r.address_id, a.label AS address_label
FROM app_ip_access_rules r
LEFT JOIN ip_addresses a ON a.id = r.address_id
WHERE r.app_id = ?
ORDER BY r.id
""",
(app_id,),
).fetchall()
con.close()
return [dict(row) for row in rows]
def get_app_ip_access_rule(rule_id: int, app_id: str):
run_migrations()
con = get_connection()
row = con.execute(
"""
SELECT r.id, r.app_id, r.ip_cidr, r.methods, r.description,
COALESCE(r.is_enabled, 1) AS is_enabled, r.created_at, r.updated_at,
r.address_id, a.label AS address_label
FROM app_ip_access_rules r
LEFT JOIN ip_addresses a ON a.id = r.address_id
WHERE r.id = ? AND r.app_id = ?
""",
(rule_id, app_id),
).fetchone()
con.close()
return dict(row) if row else None
def create_app_ip_access_rule(app_id: str, ip_cidr: str, methods: str, description: str, is_enabled: bool):
run_migrations()
con = get_connection()
con.execute(
"""
INSERT INTO app_ip_access_rules (app_id, ip_cidr, methods, description, is_enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""",
(app_id, ip_cidr, methods, description, 1 if is_enabled else 0),
)
con.commit()
con.close()
def update_app_ip_access_rule(rule_id: int, app_id: str, ip_cidr: str, methods: str, description: str, is_enabled: bool):
run_migrations()
con = get_connection()
con.execute(
"""
UPDATE app_ip_access_rules
SET ip_cidr = ?,
methods = ?,
description = ?,
is_enabled = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND app_id = ?
""",
(ip_cidr, methods, description, 1 if is_enabled else 0, rule_id, app_id),
)
con.commit()
con.close()
def delete_app_ip_access_rule(rule_id: int, app_id: str):
run_migrations()
con = get_connection()
con.execute(
"""
DELETE FROM app_ip_access_rules
WHERE id = ? AND app_id = ?
""",
(rule_id, app_id),
)
con.commit()
con.close()
def update_app_resources(app_id: str, memory: str, cpus: str):
run_migrations()
con = get_connection()
con.execute(
"""
UPDATE apps
SET memory = ?,
cpus = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(
memory or None,
cpus or None,
app_id,
),
)
con.commit()
con.close()
def build_deployment_filters(
status: str | None = None,
app_id: str | None = None,
source: str | None = None,
):
filters = []
params = []
if status:
normalized_status = status.strip().lower()
if normalized_status == "success":
filters.append(f"LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in SUCCESS_STATUSES)})")
params.extend(SUCCESS_STATUSES)
elif normalized_status == "failed":
filters.append(f"LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in FAILED_STATUSES)})")
params.extend(FAILED_STATUSES)
elif normalized_status == "running":
filters.append(f"LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in RUNNING_STATUSES)})")
params.extend(RUNNING_STATUSES)
else:
filters.append("LOWER(status) = LOWER(?)")
params.append(status)
if app_id:
filters.append("app_id = ?")
params.append(app_id)
if source:
filters.append("trigger_source = ?")
params.append(source)
where = f"WHERE {' AND '.join(filters)}" if filters else ""
return where, params
def get_deployments(
limit: int = 20,
offset: int = 0,
status: str | None = None,
app_id: str | None = None,
source: str | None = None,
):
con = get_connection()
where, params = build_deployment_filters(status=status, app_id=app_id, source=source)
rows = con.execute(
f"""
SELECT
id,
app_id,
kind,
status,
started_at,
finished_at,
returncode,
trigger_source,
triggered_by_username,
triggered_by_display_name
FROM deployments
{where}
ORDER BY
CASE
WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in RUNNING_STATUSES)}) THEN 0
ELSE 1
END,
id DESC
LIMIT ?
OFFSET ?
""",
(*params, *RUNNING_STATUSES, limit, offset),
).fetchall()
con.close()
return [dict(row) for row in rows]
def count_deployments(
status: str | None = None,
app_id: str | None = None,
source: str | None = None,
):
con = get_connection()
where, params = build_deployment_filters(status=status, app_id=app_id, source=source)
row = con.execute(
f"""
SELECT COUNT(*) AS count
FROM deployments
{where}
""",
params,
).fetchone()
con.close()
return row["count"] if row else 0
def get_deployment_filter_options():
con = get_connection()
apps = con.execute(
"""
SELECT DISTINCT app_id
FROM deployments
WHERE app_id IS NOT NULL AND app_id != ''
ORDER BY app_id
"""
).fetchall()
sources = con.execute(
"""
SELECT DISTINCT trigger_source
FROM deployments
WHERE trigger_source IS NOT NULL AND trigger_source != ''
ORDER BY trigger_source
"""
).fetchall()
con.close()
return {
"apps": [row["app_id"] for row in apps],
"sources": [row["trigger_source"] for row in sources],
}
def get_deployment_stats():
con = get_connection()
row = con.execute(
f"""
SELECT
COUNT(*) AS total,
SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in FAILED_STATUSES)}) THEN 1 ELSE 0 END) AS failed,
SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in RUNNING_STATUSES)}) THEN 1 ELSE 0 END) AS running,
SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in SUCCESS_STATUSES)}) THEN 1 ELSE 0 END) AS success
FROM deployments
""",
(*FAILED_STATUSES, *RUNNING_STATUSES, *SUCCESS_STATUSES),
).fetchone()
con.close()
stats = dict(row) if row else {"total": 0, "failed": 0, "running": 0, "success": 0}
total = stats.get("total") or 0
success = stats.get("success") or 0
stats["success_rate"] = round((success / total) * 100, 1) if total else 0
return stats
def get_app_deployments(app_id: str, limit: int = 10):
con = get_connection()
rows = con.execute(
"""
SELECT
id,
app_id,
kind,
status,
started_at,
finished_at,
returncode,
trigger_source,
triggered_by_username,
triggered_by_display_name
FROM deployments
WHERE app_id = ?
ORDER BY id DESC
LIMIT ?
""",
(app_id, limit),
).fetchall()
con.close()
return [dict(row) for row in rows]
def get_deployment(deployment_id: int):
con = get_connection()
row = con.execute(
"""
SELECT
id,
app_id,
kind,
status,
started_at,
finished_at,
returncode,
stdout,
stderr,
trigger_source,
triggered_by_username,
triggered_by_display_name,
commit_author,
pusher
FROM deployments
WHERE id = ?
""",
(deployment_id,),
).fetchone()
con.close()
return dict(row) if row else None