titan-iac/services/hermes/model-gate-configmap.yaml

152 lines
5.5 KiB
YAML

# services/hermes/model-gate-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hermes-model-gate
namespace: hermes
data:
model_gate.py: |
#!/usr/bin/env python3
"""Fail-closed proxy that admits local inference only while Hermes owns titan-24."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import ssl
import threading
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
LISTEN_HOST = os.environ.get("LISTEN_HOST", "0.0.0.0")
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8080"))
UPSTREAM_URL = os.environ.get("UPSTREAM_URL", "http://hermes-ollama.hermes.svc.cluster.local:11434").rstrip("/")
LEASE_NAMESPACE = os.environ.get("LEASE_NAMESPACE", "hermes")
LEASE_NAME = os.environ.get("LEASE_NAME", "titan-24-gpu-owner")
CACHE_TTL_SEC = float(os.environ.get("LEASE_CACHE_TTL_SEC", "1"))
API_HOST = os.environ.get("KUBERNETES_SERVICE_HOST", "kubernetes.default.svc")
API_PORT = os.environ.get("KUBERNETES_SERVICE_PORT_HTTPS", "443")
TOKEN_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
CA_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
LEASE_URL = (
f"https://{API_HOST}:{API_PORT}/apis/coordination.k8s.io/v1/"
f"namespaces/{LEASE_NAMESPACE}/leases/{LEASE_NAME}"
)
_cache_lock = threading.Lock()
_cached_owner = "unavailable"
_cached_at = 0.0
def _lease_owner() -> str:
"""Return the current owner, failing closed when Kubernetes is unavailable."""
global _cached_at, _cached_owner
now = time.monotonic()
with _cache_lock:
if now - _cached_at < CACHE_TTL_SEC:
return _cached_owner
try:
token = TOKEN_PATH.read_text(encoding="utf-8").strip()
request = Request(LEASE_URL, headers={"Authorization": f"Bearer {token}"})
context = ssl.create_default_context(cafile=str(CA_PATH))
with urlopen(request, timeout=3, context=context) as response:
payload = json.load(response)
owner = str((payload.get("spec") or {}).get("holderIdentity") or "unavailable").strip()
except Exception:
owner = "unavailable"
_cached_owner = owner
_cached_at = now
return owner
class Handler(BaseHTTPRequestHandler):
"""Proxy local model traffic while exposing health and ownership status."""
protocol_version = "HTTP/1.1"
def _json(self, status: int, payload: dict) -> None:
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def _local_allowed(self) -> tuple[bool, str]:
owner = _lease_owner()
return owner == "hermes", owner
def _proxy(self) -> None:
allowed, owner = self._local_allowed()
if not allowed:
self._json(
503,
{
"error": {
"message": f"local GPU inference unavailable while titan-24 owner is {owner}",
"type": "server_error",
},
"gpu_owner": owner,
"fallback_required": True,
},
)
return
length = int(self.headers.get("Content-Length", "0") or 0)
body = self.rfile.read(length) if length else None
headers = {"Content-Type": self.headers.get("Content-Type", "application/json")}
if self.headers.get("Accept"):
headers["Accept"] = self.headers["Accept"]
request = Request(f"{UPSTREAM_URL}{self.path}", data=body, headers=headers, method=self.command)
try:
response = urlopen(request, timeout=1800)
except HTTPError as exc:
response = exc
except (TimeoutError, URLError) as exc:
self._json(503, {"error": {"message": f"local model upstream unavailable: {exc}", "type": "server_error"}})
return
self.send_response(response.status)
content_type = response.headers.get("Content-Type")
if content_type:
self.send_header("Content-Type", content_type)
content_length = response.headers.get("Content-Length")
if content_length:
self.send_header("Content-Length", content_length)
else:
self.send_header("Connection", "close")
self.close_connection = True
self.send_header("Cache-Control", "no-store")
self.end_headers()
while True:
chunk = response.read(65536)
if not chunk:
break
self.wfile.write(chunk)
self.wfile.flush()
response.close()
def do_GET(self) -> None:
if self.path == "/healthz":
self._json(200, {"status": "ok"})
return
if self.path == "/gate/status":
allowed, owner = self._local_allowed()
self._json(200, {"gpu_owner": owner, "local_inference_allowed": allowed})
return
self._proxy()
def do_POST(self) -> None:
self._proxy()
def log_message(self, format_string: str, *args) -> None:
print(f"model-gate {self.address_string()} {format_string % args}", flush=True)
if __name__ == "__main__":
ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), Handler).serve_forever()