|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from devplacepy.database import db, get_table
|
|
from devplacepy.utils import generate_uid
|
|
|
|
from . import naming
|
|
|
|
TUNNELS_TABLE = "tunnels"
|
|
|
|
STATUS_PENDING = "pending"
|
|
STATUS_PROVISIONING = "provisioning"
|
|
STATUS_ACTIVE = "active"
|
|
STATUS_FAILED = "failed"
|
|
STATUS_SUSPENDED = "suspended"
|
|
|
|
SERVING_STATUSES = (STATUS_PROVISIONING, STATUS_ACTIVE)
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _table():
|
|
return get_table(TUNNELS_TABLE)
|
|
|
|
|
|
def list_for_instance(instance_uid: str) -> list[dict]:
|
|
return list(
|
|
_table().find(
|
|
instance_uid=instance_uid, deleted_at=None, order_by=["created_at"]
|
|
)
|
|
)
|
|
|
|
|
|
def list_for_user(user_uid: str) -> list[dict]:
|
|
return list(
|
|
_table().find(user_uid=user_uid, deleted_at=None, order_by=["-created_at"])
|
|
)
|
|
|
|
|
|
def get(uid: str) -> dict | None:
|
|
return _table().find_one(uid=uid, deleted_at=None)
|
|
|
|
|
|
def by_hostname(hostname: str) -> dict | None:
|
|
if not hostname:
|
|
return None
|
|
bare = hostname.split(":", 1)[0].lower().rstrip(".")
|
|
return _table().find_one(hostname=bare, deleted_at=None)
|
|
|
|
|
|
def count_for_instance(instance_uid: str) -> int:
|
|
return _table().count(instance_uid=instance_uid, deleted_at=None)
|
|
|
|
|
|
def create(
|
|
instance: dict, label: str, container_port: int, user_uid: str
|
|
) -> dict | None:
|
|
name = instance.get("tunnel_name", "")
|
|
if not name or container_port <= 0:
|
|
return None
|
|
hostname = naming.hostname_for(name, container_port)
|
|
table = _table()
|
|
revived = table.find_one(hostname=hostname)
|
|
stamp = _now()
|
|
if revived:
|
|
table.update(
|
|
{
|
|
"uid": revived["uid"],
|
|
"instance_uid": instance["uid"],
|
|
"project_uid": instance.get("project_uid", ""),
|
|
"user_uid": user_uid,
|
|
"label": label or f"port {container_port}",
|
|
"container_port": container_port,
|
|
"desired_state": "present",
|
|
"status": STATUS_PENDING,
|
|
"last_error": "",
|
|
"updated_at": stamp,
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
},
|
|
["uid"],
|
|
)
|
|
return table.find_one(uid=revived["uid"])
|
|
uid = generate_uid()
|
|
table.insert(
|
|
{
|
|
"uid": uid,
|
|
"instance_uid": instance["uid"],
|
|
"project_uid": instance.get("project_uid", ""),
|
|
"user_uid": user_uid,
|
|
"hostname": hostname,
|
|
"label": label or f"port {container_port}",
|
|
"container_port": container_port,
|
|
"desired_state": "present",
|
|
"status": STATUS_PENDING,
|
|
"cert_status": "",
|
|
"cert_checked_at": "",
|
|
"request_count": 0,
|
|
"bytes_out": 0,
|
|
"last_request_at": "",
|
|
"last_error": "",
|
|
"last_synced_at": "",
|
|
"created_at": stamp,
|
|
"updated_at": stamp,
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
}
|
|
)
|
|
return table.find_one(uid=uid)
|
|
|
|
|
|
def update(uid: str, changes: dict) -> None:
|
|
_table().update({"uid": uid, "updated_at": _now(), **changes}, ["uid"])
|
|
|
|
|
|
def mark_absent(uid: str, actor_uid: str = "system") -> None:
|
|
update(uid, {"desired_state": "absent"})
|
|
|
|
|
|
def soft_delete(uid: str, actor_uid: str = "system") -> None:
|
|
_table().update(
|
|
{"uid": uid, "deleted_at": _now(), "deleted_by": actor_uid}, ["uid"]
|
|
)
|
|
|
|
|
|
def suspend_for_instance(instance_uid: str) -> None:
|
|
for row in list_for_instance(instance_uid):
|
|
update(row["uid"], {"status": STATUS_SUSPENDED})
|
|
|
|
|
|
def resume_for_instance(instance_uid: str) -> None:
|
|
for row in list_for_instance(instance_uid):
|
|
if row.get("status") == STATUS_SUSPENDED:
|
|
update(row["uid"], {"status": STATUS_PENDING})
|
|
|
|
|
|
def record_hit(uid: str, byte_count: int) -> None:
|
|
from sqlalchemy import text
|
|
|
|
sql = (
|
|
"UPDATE tunnels SET request_count = COALESCE(request_count, 0) + 1, "
|
|
"bytes_out = COALESCE(bytes_out, 0) + :bytes, last_request_at = :seen "
|
|
"WHERE uid = :uid AND deleted_at IS NULL"
|
|
)
|
|
with db:
|
|
db.executable.execute(
|
|
text(sql),
|
|
{"bytes": max(0, byte_count), "seen": _now(), "uid": uid},
|
|
)
|