# retoor <retoor@molodetz.nl>
import hashlib
import json
from datetime import datetime, timezone
from devplacepy.database import db, get_table
from devplacepy.utils import generate_uid, make_combined_slug
DF_ACTIVE = "active"
DF_ARCHIVED = "archived"
BUILD_QUEUED = "queued"
BUILD_BUILDING = "building"
BUILD_SUCCESS = "success"
BUILD_FAILED = "failed"
BUILD_CANCELLED = "cancelled"
ST_CREATED = "created"
ST_STARTING = "starting"
ST_RUNNING = "running"
ST_STOPPED = "stopped"
ST_PAUSED = "paused"
ST_CRASHED = "crashed"
ST_RESTARTING = "restarting"
ST_REMOVING = "removing"
ST_REMOVED = "removed"
DESIRED_RUNNING = "running"
DESIRED_STOPPED = "stopped"
DESIRED_PAUSED = "paused"
RESTART_POLICIES = ("never", "always", "on-failure", "unless-stopped")
METRICS_RING = 720
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def content_hash(text: str) -> str:
return hashlib.sha256((text or "").encode("utf-8")).hexdigest()
def _exists(table: str) -> bool:
return table in db.tables
# ---------------- dockerfiles ----------------
def create_dockerfile(project_uid, user, name, description, tags) -> dict:
uid = generate_uid()
row = {
"uid": uid, "project_uid": project_uid, "user_uid": user["uid"],
"name": name, "slug": make_combined_slug(name, uid),
"description": description or "", "tags": tags or "", "author": user.get("username", ""),
"current_version": 0, "latest_build_number": 0,
"content_hash": "", "status": DF_ACTIVE,
"created_at": now(), "updated_at": now(),
}
get_table("dockerfiles").insert(row)
return get_dockerfile(uid)
def get_dockerfile(uid_or_slug: str):
table = get_table("dockerfiles")
if not _exists("dockerfiles"):
return None
return table.find_one(uid=uid_or_slug) or table.find_one(slug=uid_or_slug)
def list_dockerfiles(project_uid: str) -> list:
if not _exists("dockerfiles"):
return []
return sorted(get_table("dockerfiles").find(project_uid=project_uid),
key=lambda r: r.get("created_at", ""), reverse=True)
def update_dockerfile(uid: str, changes: dict) -> None:
get_table("dockerfiles").update({"uid": uid, "updated_at": now(), **changes}, ["uid"])
# ---------------- versions (immutable) ----------------
def create_version(dockerfile: dict, content: str, user: dict) -> dict:
version = int(dockerfile.get("current_version") or 0) + 1
uid = generate_uid()
get_table("dockerfile_versions").insert({
"uid": uid, "dockerfile_uid": dockerfile["uid"], "project_uid": dockerfile["project_uid"],
"version": version, "content": content, "content_hash": content_hash(content),
"created_at": now(), "created_by": user["uid"],
})
update_dockerfile(dockerfile["uid"], {"current_version": version, "content_hash": content_hash(content)})
return get_table("dockerfile_versions").find_one(uid=uid)
def get_version(uid: str):
if not _exists("dockerfile_versions"):
return None
return get_table("dockerfile_versions").find_one(uid=uid)
def current_version(dockerfile: dict):
if not _exists("dockerfile_versions"):
return None
return get_table("dockerfile_versions").find_one(
dockerfile_uid=dockerfile["uid"], version=int(dockerfile.get("current_version") or 0))
def list_versions(dockerfile_uid: str) -> list:
if not _exists("dockerfile_versions"):
return []
return sorted(get_table("dockerfile_versions").find(dockerfile_uid=dockerfile_uid),
key=lambda r: r.get("version", 0), reverse=True)
# ---------------- builds ----------------
def create_build(dockerfile: dict, version: dict, build_number: int, image_tag: str, also_latest: bool) -> dict:
uid = generate_uid()
get_table("builds").insert({
"uid": uid, "dockerfile_uid": dockerfile["uid"], "dockerfile_version_uid": version["uid"],
"project_uid": dockerfile["project_uid"], "build_number": build_number,
"image_tag": image_tag, "also_latest": 1 if also_latest else 0,
"status": BUILD_QUEUED, "job_uid": "", "image_id": "", "logs": "", "error": "",
"duration_ms": 0, "queued_at": now(), "started_at": "", "completed_at": "", "created_at": now(),
})
return get_build(uid)
def get_build(uid: str):
if not _exists("builds"):
return None
return get_table("builds").find_one(uid=uid)
def update_build(uid: str, changes: dict) -> None:
get_table("builds").update({"uid": uid, **changes}, ["uid"])
def list_builds(dockerfile_uid: str, limit: int = 50) -> list:
if not _exists("builds"):
return []
rows = sorted(get_table("builds").find(dockerfile_uid=dockerfile_uid),
key=lambda r: r.get("build_number", 0), reverse=True)
return rows[:limit]
# ---------------- instances ----------------
def create_instance(row: dict) -> dict:
uid = generate_uid()
base = {
"uid": uid, "slug": make_combined_slug(row.get("name", "instance"), uid),
"container_id": "", "status": ST_CREATED, "exit_code": 0, "restart_count": 0,
"ingress_slug": "", "ingress_port": 0,
"started_at": "", "stopped_at": "", "created_at": now(), "updated_at": now(),
}
base.update(row)
base["uid"] = uid
get_table("instances").insert(base)
return get_instance(uid)
def get_instance(uid: str):
table = get_table("instances")
if not _exists("instances"):
return None
return table.find_one(uid=uid) or table.find_one(slug=uid)
def list_instances(project_uid: str = None, dockerfile_uid: str = None) -> list:
if not _exists("instances"):
return []
filters = {}
if project_uid:
filters["project_uid"] = project_uid
if dockerfile_uid:
filters["dockerfile_uid"] = dockerfile_uid
return sorted(get_table("instances").find(**filters),
key=lambda r: r.get("created_at", ""), reverse=True)
def all_instances() -> list:
if not _exists("instances"):
return []
return list(get_table("instances").find())
def find_instance_by_ingress(slug: str):
if not slug or not _exists("instances"):
return None
matches = list(get_table("instances").find(ingress_slug=slug))
for instance in matches:
if instance.get("status") == ST_RUNNING:
return instance
return matches[0] if matches else None
def update_instance(uid: str, changes: dict) -> None:
get_table("instances").update({"uid": uid, "updated_at": now(), **changes}, ["uid"])
def delete_instance(uid: str) -> None:
get_table("instances").delete(uid=uid)
# ---------------- events / metrics / schedules ----------------
def record_event(instance: dict, event: str, actor_kind: str, actor_id: str, detail: dict = None) -> None:
get_table("instance_events").insert({
"uid": generate_uid(), "instance_uid": instance["uid"], "project_uid": instance.get("project_uid", ""),
"event": event, "actor_kind": actor_kind, "actor_id": actor_id or "",
"detail": json.dumps(detail or {}), "created_at": now(),
})
def list_events(instance_uid: str, limit: int = 100) -> list:
if not _exists("instance_events"):
return []
rows = sorted(get_table("instance_events").find(instance_uid=instance_uid),
key=lambda r: r.get("created_at", ""), reverse=True)
return rows[:limit]
def insert_metric(instance_uid: str, sample) -> None:
table = get_table("instance_metrics")
table.insert({
"uid": generate_uid(), "instance_uid": instance_uid, "ts": now(),
"cpu_pct": sample.cpu_pct, "mem_bytes": sample.mem_bytes, "mem_pct": sample.mem_pct,
"net_rx": sample.net_rx, "net_tx": sample.net_tx,
"blk_read": sample.blk_read, "blk_write": sample.blk_write,
})
rows = sorted(table.find(instance_uid=instance_uid), key=lambda r: r.get("ts", ""))
if len(rows) > METRICS_RING:
for old in rows[:len(rows) - METRICS_RING]:
table.delete(uid=old["uid"])
def recent_metrics(instance_uid: str, limit: int = 120) -> list:
if not _exists("instance_metrics"):
return []
rows = sorted(get_table("instance_metrics").find(instance_uid=instance_uid),
key=lambda r: r.get("ts", ""))
return rows[-limit:]
def create_schedule(instance: dict, action: str, schedule_columns: dict, next_run_at: str) -> dict:
uid = generate_uid()
get_table("instance_schedules").insert({
"uid": uid, "instance_uid": instance["uid"], "project_uid": instance.get("project_uid", ""),
"action": action, "schedule_json": json.dumps(schedule_columns), "enabled": 1,
"next_run_at": next_run_at, "last_run_at": "", "run_count": 0,
"created_at": now(), "updated_at": now(),
})
return get_table("instance_schedules").find_one(uid=uid)
def list_schedules(instance_uid: str) -> list:
if not _exists("instance_schedules"):
return []
return list(get_table("instance_schedules").find(instance_uid=instance_uid))
def due_schedules(now_iso: str) -> list:
if not _exists("instance_schedules"):
return []
return [r for r in get_table("instance_schedules").find(enabled=1)
if r.get("next_run_at") and r["next_run_at"] <= now_iso]
def update_schedule(uid: str, changes: dict) -> None:
get_table("instance_schedules").update({"uid": uid, "updated_at": now(), **changes}, ["uid"])
def delete_schedule(uid: str) -> None:
get_table("instance_schedules").delete(uid=uid)