152 lines
5.5 KiB
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()
|