89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
"""Permanent, secret-free synthetic monitoring for the governed Gitea surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import urllib.error
|
|
import urllib.request
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml # type: ignore[import-untyped]
|
|
|
|
_GITEA_READY_URL = "http://connector-gitea:8080/health/ready"
|
|
_FINGERPRINT = re.compile(r"^[0-9a-f]{64}$")
|
|
_MAX_RESPONSE_BYTES = 32 * 1024
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SyntheticSnapshot:
|
|
tool_count: int
|
|
capability_fingerprint: str
|
|
upstream: str
|
|
|
|
|
|
def expected_gitea_wire_tools(catalog_root: Path) -> frozenset[str]:
|
|
names: set[str] = set()
|
|
for path in sorted((catalog_root / "tools").glob("*.yaml")):
|
|
value = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
spec = value.get("spec", {}) if isinstance(value, dict) else {}
|
|
if spec.get("connectorType") != "gitea":
|
|
continue
|
|
name = spec.get("connectorToolName", spec.get("canonicalName"))
|
|
if not isinstance(name, str) or not name:
|
|
raise ValueError("Gitea catalog carries an invalid connector tool name")
|
|
names.add(name)
|
|
if not names:
|
|
raise ValueError("Gitea synthetic monitor has no expected tools")
|
|
return frozenset(names)
|
|
|
|
|
|
class GiteaSyntheticMonitor:
|
|
"""Probe upstream reachability and detect capability drift every worker cycle."""
|
|
|
|
def __init__(self, catalog_root: Path) -> None:
|
|
self.expected_tools = expected_gitea_wire_tools(catalog_root)
|
|
self._baseline_fingerprint: str | None = None
|
|
|
|
def probe(self) -> SyntheticSnapshot:
|
|
request = urllib.request.Request( # noqa: S310 - fixed internal allowlisted endpoint
|
|
_GITEA_READY_URL,
|
|
headers={"Accept": "application/json"},
|
|
method="GET",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=5) as response: # noqa: S310
|
|
raw = response.read(_MAX_RESPONSE_BYTES + 1)
|
|
status = int(response.status)
|
|
except (OSError, urllib.error.URLError) as exc:
|
|
raise RuntimeError("Gitea synthetic readiness probe failed") from exc
|
|
if status != 200 or len(raw) > _MAX_RESPONSE_BYTES:
|
|
raise RuntimeError("Gitea synthetic readiness response is invalid")
|
|
try:
|
|
payload: Any = json.loads(raw)
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise RuntimeError("Gitea synthetic readiness response is invalid") from exc
|
|
if not isinstance(payload, dict):
|
|
raise RuntimeError("Gitea synthetic readiness response is invalid")
|
|
fingerprint = payload.get("capabilityFingerprint")
|
|
tool_count = payload.get("toolCount")
|
|
if (
|
|
payload.get("status") != "ready"
|
|
or payload.get("upstream") != "reachable"
|
|
or not isinstance(tool_count, int)
|
|
or tool_count != len(self.expected_tools)
|
|
or not isinstance(fingerprint, str)
|
|
or not _FINGERPRINT.fullmatch(fingerprint)
|
|
):
|
|
raise RuntimeError("Gitea synthetic capability contract drifted")
|
|
if self._baseline_fingerprint is not None and fingerprint != self._baseline_fingerprint:
|
|
raise RuntimeError("Gitea synthetic capability fingerprint changed unexpectedly")
|
|
self._baseline_fingerprint = fingerprint
|
|
return SyntheticSnapshot(
|
|
tool_count=tool_count,
|
|
capability_fingerprint=fingerprint,
|
|
upstream="reachable",
|
|
)
|