项目文件夹

文件
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 行
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")