|
# 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)
|