b05f245299
Second slice of PRD 0070, still a backend-neutral dev-harness:
* orchestrator/broker.py — the launch-broker contract. A LaunchRequest is
structured (static ids/flags only — bottle id, pool slot, a
content-addressed image_ref; never a path/argv) and signed as a compact
HS256 JWT so the broker verifies PROVENANCE before acting: a compromised
co-located component can't forge a launch without the shared secret.
verify_request is fail-closed (bad sig / malformed / off-schema -> raise).
Stdlib only (no runtime deps). Ships a StubBroker that records verified
requests for the harness/tests.
* orchestrator/service.py — the Orchestrator: owns the registry and brokers
the lifecycle. launch_bottle mints the bottle + sends a signed launch,
rolling the registry entry back if the launch fails (no orphans);
teardown_bottle brokers teardown then deregisters; attribute delegates.
* control_plane.py — POST /bottles now launches, DELETE tears down (both go
through the Orchestrator + broker). dispatch/server take an Orchestrator.
* __main__.py wires an ephemeral secret + StubBroker for the harness.
Tests: broker sign/verify round-trip, tamper/wrong-secret/malformed/off-schema
rejection, StubBroker fail-closed; Orchestrator launch->registry->attribute,
teardown, rollback-on-broker-failure; control-plane updated for launch/teardown.
Full suite green (only the pre-existing /bin/sleep errors); harness does
launch -> attribute -> teardown over HTTP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
87 lines
3.3 KiB
Python
87 lines
3.3 KiB
Python
"""Unit tests for the orchestrator launch broker (PRD 0070)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
import unittest
|
|
|
|
from bot_bottle.orchestrator.broker import (
|
|
BrokerAuthError,
|
|
LaunchRequest,
|
|
StubBroker,
|
|
sign_request,
|
|
verify_request,
|
|
)
|
|
|
|
|
|
class TestSignVerify(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.secret = secrets.token_bytes(16)
|
|
|
|
def test_launch_round_trip(self) -> None:
|
|
req = LaunchRequest(
|
|
op="launch", bottle_id="b1", source_ip="10.243.0.1",
|
|
image_ref="sha256:abc", slot=3,
|
|
)
|
|
self.assertEqual(req, verify_request(sign_request(req, self.secret), self.secret))
|
|
|
|
def test_teardown_round_trip(self) -> None:
|
|
req = LaunchRequest(op="teardown", bottle_id="b1")
|
|
got = verify_request(sign_request(req, self.secret), self.secret)
|
|
self.assertEqual(("teardown", "b1"), (got.op, got.bottle_id))
|
|
|
|
def test_wrong_secret_rejected(self) -> None:
|
|
token = sign_request(LaunchRequest(op="launch", bottle_id="b1"), self.secret)
|
|
with self.assertRaises(BrokerAuthError):
|
|
verify_request(token, secrets.token_bytes(16))
|
|
|
|
def test_tampered_payload_rejected(self) -> None:
|
|
token = sign_request(LaunchRequest(op="launch", bottle_id="b1"), self.secret)
|
|
header, payload, sig = token.split(".")
|
|
flipped = payload[:-1] + ("A" if payload[-1] != "A" else "B")
|
|
with self.assertRaises(BrokerAuthError):
|
|
verify_request(f"{header}.{flipped}.{sig}", self.secret)
|
|
|
|
def test_malformed_tokens_rejected(self) -> None:
|
|
for bad in ("", "a.b", "a.b.c.d", "not-a-token"):
|
|
with self.assertRaises(BrokerAuthError):
|
|
verify_request(bad, self.secret)
|
|
|
|
def test_unknown_op_rejected_despite_valid_signature(self) -> None:
|
|
# A correctly-signed token whose op isn't in the allow-list must
|
|
# still be refused — the schema guard is independent of provenance.
|
|
token = sign_request(LaunchRequest(op="rm-rf", bottle_id="b1"), self.secret)
|
|
with self.assertRaises(BrokerAuthError):
|
|
verify_request(token, self.secret)
|
|
|
|
|
|
class TestStubBroker(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.secret = secrets.token_bytes(16)
|
|
self.broker = StubBroker(self.secret)
|
|
|
|
def test_submit_launch_records_verified_request(self) -> None:
|
|
req = LaunchRequest(op="launch", bottle_id="b1", source_ip="10.243.0.1")
|
|
got = self.broker.submit(sign_request(req, self.secret))
|
|
self.assertEqual(req, got)
|
|
self.assertEqual(["b1"], [r.bottle_id for r in self.broker.launched])
|
|
self.assertEqual([], self.broker.torn_down)
|
|
|
|
def test_submit_teardown_records(self) -> None:
|
|
self.broker.submit(
|
|
sign_request(LaunchRequest(op="teardown", bottle_id="b1"), self.secret)
|
|
)
|
|
self.assertEqual(["b1"], [r.bottle_id for r in self.broker.torn_down])
|
|
|
|
def test_submit_forged_token_is_fail_closed(self) -> None:
|
|
forged = sign_request(
|
|
LaunchRequest(op="launch", bottle_id="b1"), secrets.token_bytes(16)
|
|
)
|
|
with self.assertRaises(BrokerAuthError):
|
|
self.broker.submit(forged)
|
|
self.assertEqual([], self.broker.launched) # nothing acted on
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|