fix(hermes): reject invalid release revisions early
This commit is contained in:
parent
33de8e38fe
commit
3c06ae867b
@ -14,6 +14,10 @@ from pathlib import Path
|
||||
|
||||
JENKINS_ORIGIN = "https://ci.bstein.dev"
|
||||
JENKINS_BUILD_URL = f"{JENKINS_ORIGIN}/buildByToken/buildWithParameters"
|
||||
SCM_BROKER_ORIGIN = "http://hermes-scm-broker.hermes-scm.svc.cluster.local:9081"
|
||||
SCM_METADATA_URL = f"{SCM_BROKER_ORIGIN}/v1/metadata"
|
||||
SCM_REPOSITORY = "titan-iac"
|
||||
MAX_SCM_RESPONSE_BYTES = 512 * 1024
|
||||
JOBS = {
|
||||
"agent": {
|
||||
"job": "hermes-agent-image",
|
||||
@ -58,12 +62,45 @@ def _open_without_redirect(request: urllib.request.Request, timeout: int):
|
||||
raise
|
||||
|
||||
|
||||
def _verify_reviewed_revision_exists(revision: str) -> None:
|
||||
"""Fail before queuing when the exact reviewed commit is absent from SCM."""
|
||||
path = f"/api/v1/repos/atlas/{SCM_REPOSITORY}/git/commits/{revision}"
|
||||
payload = json.dumps({"path": path}, separators=(",", ":")).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
SCM_METADATA_URL,
|
||||
data=payload,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "hermes-image-release/1",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
opener = urllib.request.build_opener(_NoRedirect())
|
||||
with opener.open(request, timeout=20) as response:
|
||||
if int(response.status) != 200:
|
||||
raise RuntimeError("SCM did not confirm the reviewed revision")
|
||||
content_type = response.headers.get_content_type()
|
||||
if content_type != "application/json":
|
||||
raise RuntimeError("SCM returned an unexpected revision response")
|
||||
body = response.read(MAX_SCM_RESPONSE_BYTES + 1)
|
||||
if len(body) > MAX_SCM_RESPONSE_BYTES:
|
||||
raise RuntimeError("SCM revision response exceeded the safe size limit")
|
||||
try:
|
||||
result = json.loads(body)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("SCM returned malformed revision evidence") from exc
|
||||
if not isinstance(result, dict) or result.get("sha") != revision:
|
||||
raise RuntimeError("SCM did not confirm the exact reviewed revision")
|
||||
|
||||
|
||||
def trigger_build(
|
||||
revision: str,
|
||||
*,
|
||||
component: str = "agent",
|
||||
token_file: Path = TOKEN_FILE,
|
||||
opener=_open_without_redirect,
|
||||
revision_verifier=None,
|
||||
) -> dict[str, str | int]:
|
||||
"""Post one allow-listed job's fixed parameters using the release token."""
|
||||
revision = revision.strip()
|
||||
@ -75,6 +112,8 @@ def trigger_build(
|
||||
token = token_file.read_text(encoding="utf-8").strip()
|
||||
if not token:
|
||||
raise RuntimeError("Jenkins image-build token is empty")
|
||||
if revision_verifier is not None:
|
||||
revision_verifier(revision)
|
||||
fields = {
|
||||
"job": job["job"],
|
||||
"token": token,
|
||||
@ -132,7 +171,11 @@ def main() -> int:
|
||||
parser.add_argument("revision", help="reviewed full commit contained by main")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
result = trigger_build(args.revision, component=args.component)
|
||||
result = trigger_build(
|
||||
args.revision,
|
||||
component=args.component,
|
||||
revision_verifier=_verify_reviewed_revision_exists,
|
||||
)
|
||||
except (OSError, ValueError, RuntimeError, urllib.error.URLError) as exc:
|
||||
print(json.dumps({"error": str(exc)}, sort_keys=True))
|
||||
return 1
|
||||
|
||||
@ -190,6 +190,60 @@ def test_trigger_redirect_wrapper_accepts_only_plugin_303(
|
||||
module._open_without_redirect(request, 9)
|
||||
|
||||
|
||||
def test_trigger_confirms_exact_revision_through_credential_isolated_scm(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Revision preflight is bounded, fixed-origin, and exact-SHA fail closed."""
|
||||
module = _load(TRIGGER, "trigger_revision_coverage")
|
||||
revision = "7" * 40
|
||||
captured = {}
|
||||
|
||||
class Headers(dict):
|
||||
def get_content_type(self):
|
||||
return self.get("Content-Type", "application/octet-stream")
|
||||
|
||||
class Opener:
|
||||
def __init__(self, status=200, body=b"", content_type="application/json"):
|
||||
self.status = status
|
||||
self.body = body
|
||||
self.content_type = content_type
|
||||
|
||||
def open(self, request, timeout):
|
||||
captured["request"] = request
|
||||
captured["timeout"] = timeout
|
||||
response = Response(
|
||||
self.status,
|
||||
self.body,
|
||||
Headers({"Content-Type": self.content_type}),
|
||||
)
|
||||
return response
|
||||
|
||||
opener = Opener(body=json.dumps({"sha": revision}).encode())
|
||||
monkeypatch.setattr(module.urllib.request, "build_opener", lambda *_args: opener)
|
||||
module._verify_reviewed_revision_exists(revision)
|
||||
request = captured["request"]
|
||||
assert request.full_url == module.SCM_METADATA_URL
|
||||
assert captured["timeout"] == 20
|
||||
assert json.loads(request.data)["path"].endswith(f"/git/commits/{revision}")
|
||||
|
||||
cases = (
|
||||
(Opener(status=503, body=b"{}"), "did not confirm"),
|
||||
(Opener(body=b"{}", content_type="text/plain"), "unexpected"),
|
||||
(Opener(body=b"x" * (module.MAX_SCM_RESPONSE_BYTES + 1)), "safe size"),
|
||||
(Opener(body=b"not-json"), "malformed"),
|
||||
(Opener(body=json.dumps({"sha": "8" * 40}).encode()), "exact"),
|
||||
(Opener(body=b"[]"), "exact"),
|
||||
)
|
||||
for failing_opener, message in cases:
|
||||
monkeypatch.setattr(
|
||||
module.urllib.request,
|
||||
"build_opener",
|
||||
lambda *_args, value=failing_opener: value,
|
||||
)
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
module._verify_reviewed_revision_exists(revision)
|
||||
|
||||
|
||||
def test_trigger_main_reports_safe_success_and_errors(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
@ -200,7 +254,7 @@ def test_trigger_main_reports_safe_success_and_errors(
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"trigger_build",
|
||||
lambda value, component="agent": {
|
||||
lambda value, component="agent", **_kwargs: {
|
||||
"component": component,
|
||||
"job": module.JOBS[component]["job"],
|
||||
"source_revision": value,
|
||||
@ -212,7 +266,9 @@ def test_trigger_main_reports_safe_success_and_errors(
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"trigger_build",
|
||||
lambda _value, component="agent": (_ for _ in ()).throw(OSError("closed")),
|
||||
lambda _value, component="agent", **_kwargs: (
|
||||
_ for _ in ()
|
||||
).throw(OSError("closed")),
|
||||
)
|
||||
assert module.main() == 1
|
||||
assert json.loads(capsys.readouterr().out) == {"error": "closed"}
|
||||
|
||||
@ -322,6 +322,28 @@ def test_trigger_returns_a_token_free_exact_follow_command(tmp_path: Path) -> No
|
||||
assert "never-print-me" not in json.dumps(result)
|
||||
|
||||
|
||||
def test_trigger_cli_verifies_the_exact_revision_before_queueing(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""The CLI never spends a Jenkins build slot on a nonexistent commit."""
|
||||
module = _load(TRIGGER, "release_status_trigger_revision")
|
||||
revision = "8" * 40
|
||||
seen = []
|
||||
|
||||
def trigger(value, *, component, revision_verifier):
|
||||
revision_verifier(value)
|
||||
seen.append((value, component))
|
||||
return {"source_revision": value, "component": component}
|
||||
|
||||
monkeypatch.setattr(module, "trigger_build", trigger)
|
||||
monkeypatch.setattr(module, "_verify_reviewed_revision_exists", seen.append)
|
||||
monkeypatch.setattr(sys, "argv", ["trigger", "--component", "stt", revision])
|
||||
|
||||
assert module.main() == 0
|
||||
assert seen == [revision, (revision, "stt")]
|
||||
assert json.loads(capsys.readouterr().out)["source_revision"] == revision
|
||||
|
||||
|
||||
def test_runtime_bundle_and_guidance_require_live_convergence() -> None:
|
||||
"""The helper ships to Worker and merged code alone is explicitly insufficient."""
|
||||
kustomization = (ROOT / "services/hermes/kustomization.yaml").read_text()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user