# retoor <retoor@molodetz.nl>
from __future__ import annotations
from collections.abc import Callable
from devplacepy.database import get_setting
from devplacepy.services.containers import store
from devplacepy.stealth import stealth_async_client
from . import tunnels
API_PATH = "/api/v1"
ISSUE_TIMEOUT_SECONDS = 180.0
class CertError(Exception):
pass
def _base_url() -> str:
return (get_setting("workspace_molohttp_base_url", "") or "").rstrip("/")
def _auth_headers() -> dict[str, str]:
api_key = get_setting("workspace_molohttp_api_key", "") or ""
if api_key:
return {"x-api-key": api_key}
return {}
def _basic_auth() -> tuple[str, str] | None:
username = get_setting("workspace_molohttp_username", "") or ""
password = get_setting("workspace_molohttp_password", "") or ""
if username and password:
return (username, password)
return None
def configured() -> bool:
return bool(_base_url()) and bool(_auth_headers() or _basic_auth())
async def issue(hostname: str) -> None:
if not hostname:
raise CertError("no hostname")
base = _base_url()
if not base:
raise CertError("workspace_molohttp_base_url is not configured")
headers = _auth_headers()
auth = _basic_auth()
if not headers and not auth:
raise CertError("no molohttp credentials configured")
payload: dict[str, object] = {"domains": [hostname]}
email = get_setting("workspace_acme_email", "") or ""
if email:
payload["email"] = email
async with stealth_async_client(timeout=ISSUE_TIMEOUT_SECONDS) as client:
response = await client.post(
f"{base}{API_PATH}/certs/issue",
json=payload,
headers=headers,
auth=auth,
)
if response.status_code >= 400:
raise CertError(
f"molohttp cert issue failed ({response.status_code}): "
f"{response.text[:200]}"
)
async def certify(row: dict, log: Callable[[str], None] | None = None) -> bool:
hostname = row.get("hostname", "")
tunnels.update(
row["uid"], {"status": tunnels.STATUS_PROVISIONING, "last_error": ""}
)
try:
await issue(hostname)
except Exception as error:
tunnels.update(
row["uid"],
{"status": tunnels.STATUS_FAILED, "last_error": str(error)[:500]},
)
if log:
log(f"tunnel certificate for {hostname} failed: {error}")
return False
tunnels.update(row["uid"], {"status": tunnels.STATUS_ACTIVE, "last_error": ""})
if log:
log(f"tunnel certificate issued for {hostname}")
announce(row, hostname)
return True
def announce(row: dict, hostname: str) -> None:
from devplacepy.utils import create_notification
from . import provision
instance = store.get_instance(row.get("instance_uid", ""))
if not instance:
return
owner_uid = instance.get("workspace_owner_uid", "")
if not owner_uid:
return
url = f"https://{hostname}"
if provision.is_editor_tunnel(instance, row):
password = instance.get("editor_password") or ""
message = f"Your VS Code workspace is live at {url} (password: {password})."
else:
message = f"Tunnel {row.get('label') or hostname} is live at {url}."
create_notification(
owner_uid,
"workspace",
message,
instance["uid"],
f"/projects/{instance.get('project_uid', '')}/workspace",
)