106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Call the credential-isolated Hermes SCM broker over its fixed cluster origin."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import urllib.request
|
|
from collections.abc import Callable
|
|
|
|
import deadline_http
|
|
from gitea_api_policy import PolicyError
|
|
|
|
BROKER_ORIGIN = "http://hermes-scm-broker.hermes-scm.svc.cluster.local:9081"
|
|
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
|
|
|
|
|
|
def _open(request: urllib.request.Request, timeout: int):
|
|
"""Open one broker exchange under a killable absolute deadline.
|
|
|
|
The helper process keeps every request on its fixed in-cluster origin by
|
|
rejecting redirects before they are followed.
|
|
"""
|
|
return deadline_http.open_bounded(
|
|
request, maximum=MAX_RESPONSE_BYTES, timeout=timeout
|
|
)
|
|
|
|
|
|
def request(
|
|
endpoint: str,
|
|
payload: dict[str, object],
|
|
*,
|
|
opener: Callable[..., object] = _open,
|
|
) -> bytes:
|
|
"""Send one bounded broker operation without any repository credential."""
|
|
if endpoint not in {"/v1/metadata", "/v1/drafts", "/v1/tasks/register", "/v1/tasks/draft-update"}:
|
|
raise PolicyError("SCM broker operation is outside the client allowlist")
|
|
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
if len(body) > 64 * 1024:
|
|
raise PolicyError("SCM broker request exceeds the safe size limit")
|
|
outgoing = urllib.request.Request(
|
|
BROKER_ORIGIN + endpoint,
|
|
data=body,
|
|
method="POST",
|
|
headers={
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "hermes-scm-broker-client/1",
|
|
},
|
|
)
|
|
with opener(outgoing, timeout=30) as response: # type: ignore[attr-defined]
|
|
status = getattr(response, "status", None)
|
|
if status is None and hasattr(response, "getcode"):
|
|
status = response.getcode() # type: ignore[attr-defined]
|
|
if status != 200:
|
|
raise PolicyError("SCM broker returned an unexpected HTTP status")
|
|
content_type = response.headers.get_content_type() # type: ignore[attr-defined]
|
|
if content_type != "application/json":
|
|
raise PolicyError("SCM broker returned an unexpected response type")
|
|
result = response.read(MAX_RESPONSE_BYTES + 1) # type: ignore[attr-defined]
|
|
if len(result) > MAX_RESPONSE_BYTES:
|
|
raise PolicyError("SCM broker response exceeds the safe size limit")
|
|
json.loads(result)
|
|
return result
|
|
|
|
|
|
def read(path: str, *, opener: Callable[..., object] = _open) -> bytes:
|
|
"""Read explicitly allowed Atlas metadata through the broker."""
|
|
return request("/v1/metadata", {"path": path}, opener=opener)
|
|
|
|
|
|
def create_draft(
|
|
repo: str,
|
|
*,
|
|
base: str,
|
|
head: str,
|
|
head_sha: str,
|
|
title: str,
|
|
body: str,
|
|
opener: Callable[..., object] = _open,
|
|
) -> bytes:
|
|
"""Create one verified draft through the broker for human review."""
|
|
return request(
|
|
"/v1/drafts",
|
|
{
|
|
"base": base,
|
|
"body": body,
|
|
"head": head,
|
|
"head_sha": head_sha,
|
|
"repo": repo,
|
|
"title": title,
|
|
},
|
|
opener=opener,
|
|
)
|
|
|
|
|
|
def register_task(grant: str, *, opener: Callable[..., object] = _open) -> bytes:
|
|
"""Register the broker-owned branch before its first task push."""
|
|
if not isinstance(grant, str) or not grant:
|
|
raise PolicyError("SCM task grant is invalid")
|
|
return request("/v1/tasks/register", {"grant": grant}, opener=opener)
|
|
|
|
|
|
def update_draft(grant: str, pr_number: int, title: str, body: str, *, opener: Callable[..., object] = _open) -> bytes:
|
|
"""Refresh prose on the one broker-verified continuing task PR."""
|
|
return request("/v1/tasks/draft-update", {"grant": grant, "pr_number": pr_number, "title": title, "body": body}, opener=opener)
|