atlas-iac/services/hermes/scm-common/scripts/scm_broker_client.py

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)