Files
light-heart-labs--dreamserver/ods/extensions/services/dashboard-api/user_extensions.py
T
wehub-resource-sync 9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:33 +08:00

132 lines
4.1 KiB
Python

"""Dynamic user extension manifest scanner with TTL cache."""
import logging
import re
import threading
import time
from pathlib import Path
from typing import Any
import yaml
logger = logging.getLogger(__name__)
_HEALTH_PATH_RE = re.compile(r"^/[A-Za-z0-9/_\-.]*$")
_HEALTH_PATH_REJECT = ("..", "@", "?", "#", "http://", "https://")
_SERVICE_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
def scan_user_extension_services(
user_ext_dir: Path,
) -> dict[str, dict[str, Any]]:
"""Scan user extensions directory for enabled services with health endpoints.
Returns a dict keyed by service_id in the same format as ``SERVICES``
from config.py, compatible with ``check_service_health()``.
"""
services: dict[str, dict[str, Any]] = {}
if not user_ext_dir.is_dir():
return services
for item in sorted(user_ext_dir.iterdir()):
if item.is_symlink():
continue
if not item.is_dir():
continue
service_id = item.name
if not _SERVICE_ID_RE.match(service_id):
continue
# Only enabled extensions (compose.yaml present, not .disabled)
if not (item / "compose.yaml").exists():
continue
manifest_path = item / "manifest.yaml"
if not manifest_path.exists():
continue
try:
manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
except (yaml.YAMLError, OSError) as e:
logger.debug("Skipping %s: bad manifest: %s", service_id, e)
continue
if not isinstance(manifest, dict):
continue
svc = manifest.get("service")
if not isinstance(svc, dict):
continue
health = svc.get("health") or ""
try:
if health:
# Validate health path (must be a string)
if not isinstance(health, str):
raise TypeError(f"health must be a string, got {type(health).__name__}")
if not _HEALTH_PATH_RE.match(health):
logger.warning("Rejected health path for %s: %r", service_id, health)
continue
if any(bad in health for bad in _HEALTH_PATH_REJECT):
logger.warning("Rejected health path for %s: %r", service_id, health)
continue
port = svc.get("port", 0)
name = svc.get("name", service_id)
# Host = service_id (Docker DNS). Never trust manifest host_env/default_host.
services[service_id] = {
"host": service_id,
"port": int(port),
"external_port": int(svc.get("external_port_default", port)),
"health": health,
"name": name,
# Optional: extensions whose health endpoint lives on a
# secondary port (e.g. milvus 9091) need an explicit
# health_port; check_service_health() falls back to "port"
# when absent.
**({"health_port": int(svc["health_port"])} if "health_port" in svc else {}),
}
except (TypeError, ValueError) as exc:
logger.warning("Skipping extension %s: invalid manifest value: %s", service_id, exc)
continue
return services
# --- TTL Cache ---
_cache: dict[str, Any] = {"result": {}, "timestamp": float("-inf")}
_cache_lock = threading.Lock()
def get_user_services_cached(
user_ext_dir: Path, ttl: float = 30.0,
) -> dict[str, dict[str, Any]]:
"""Return cached result of ``scan_user_extension_services()``.
Re-scans when *ttl* seconds have elapsed since the last scan.
"""
with _cache_lock:
now = time.monotonic()
if now - _cache["timestamp"] < ttl:
return _cache["result"].copy()
result = scan_user_extension_services(user_ext_dir)
with _cache_lock:
_cache["result"] = result
_cache["timestamp"] = time.monotonic()
return result.copy()
def _reset_cache() -> None:
"""Clear the cached scan result. Used for test isolation."""
with _cache_lock:
_cache["result"] = {}
_cache["timestamp"] = float("-inf")