feat: run smolmachines sidecar as vm
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from bot_bottle.backend.egress_apply import EgressApplyError
|
||||
from bot_bottle.backend.smolmachines import egress_apply
|
||||
from bot_bottle.backend.smolmachines.smolvm import SmolvmRunResult
|
||||
|
||||
|
||||
class TestFetchCurrentRoutes(unittest.TestCase):
|
||||
def test_reads_routes_from_sidecar_vm(self):
|
||||
with patch.object(
|
||||
egress_apply._smolvm,
|
||||
"machine_exec",
|
||||
return_value=SmolvmRunResult(0, "routes", ""),
|
||||
) as exec_:
|
||||
self.assertEqual("routes", egress_apply.fetch_current_routes("dev-abc"))
|
||||
exec_.assert_called_once_with(
|
||||
"bot-bottle-sidecars-dev-abc",
|
||||
["cat", "/etc/egress/routes.yaml"],
|
||||
)
|
||||
|
||||
def test_read_failure_raises_apply_error(self):
|
||||
with patch.object(
|
||||
egress_apply._smolvm,
|
||||
"machine_exec",
|
||||
return_value=SmolvmRunResult(1, "", "nope"),
|
||||
):
|
||||
with self.assertRaises(EgressApplyError):
|
||||
egress_apply.fetch_current_routes("dev-abc")
|
||||
|
||||
|
||||
class TestReload(unittest.TestCase):
|
||||
def test_sends_hup_to_sidecar_init(self):
|
||||
with patch.object(
|
||||
egress_apply._smolvm,
|
||||
"machine_exec",
|
||||
return_value=SmolvmRunResult(0, "", ""),
|
||||
) as exec_:
|
||||
egress_apply.SmolmachinesEgressApplicator()._signal_bundle_reload("dev-abc")
|
||||
exec_.assert_called_once_with(
|
||||
"bot-bottle-sidecars-dev-abc",
|
||||
["sh", "-c", "kill -HUP 1"],
|
||||
)
|
||||
|
||||
def test_reload_failure_raises_apply_error(self):
|
||||
with patch.object(
|
||||
egress_apply._smolvm,
|
||||
"machine_exec",
|
||||
return_value=SmolvmRunResult(1, "", "nope"),
|
||||
):
|
||||
with self.assertRaises(EgressApplyError):
|
||||
egress_apply.SmolmachinesEgressApplicator()._signal_bundle_reload("dev-abc")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
from bot_bottle.backend.smolmachines.port_forward import (
|
||||
ForwardSpec,
|
||||
start_forwarder,
|
||||
stop_forwarder,
|
||||
)
|
||||
|
||||
|
||||
def _echo_server() -> tuple[socket.socket, int]:
|
||||
listener = socket.create_server(("127.0.0.1", 0))
|
||||
port = int(listener.getsockname()[1])
|
||||
|
||||
def run() -> None:
|
||||
with listener:
|
||||
conn, _ = listener.accept()
|
||||
with conn:
|
||||
while True:
|
||||
data = conn.recv(65536)
|
||||
if not data:
|
||||
return
|
||||
conn.sendall(data)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
return listener, port
|
||||
|
||||
|
||||
class TestForwarderValidation(unittest.TestCase):
|
||||
def test_rejects_generic_localhost_listener(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
start_forwarder((
|
||||
ForwardSpec(
|
||||
label="egress",
|
||||
listen_host="127.0.0.1",
|
||||
listen_port=0,
|
||||
target_host="127.0.0.1",
|
||||
target_port=9099,
|
||||
),
|
||||
))
|
||||
|
||||
def test_rejects_wildcard_listener(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
start_forwarder((
|
||||
ForwardSpec(
|
||||
label="egress",
|
||||
listen_host="0.0.0.0",
|
||||
listen_port=0,
|
||||
target_host="127.0.0.1",
|
||||
target_port=9099,
|
||||
),
|
||||
))
|
||||
|
||||
def test_rejects_non_loopback_target(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
start_forwarder((
|
||||
ForwardSpec(
|
||||
label="egress",
|
||||
listen_host="127.0.0.16",
|
||||
listen_port=0,
|
||||
target_host="192.0.2.10",
|
||||
target_port=9099,
|
||||
),
|
||||
))
|
||||
|
||||
|
||||
class TestForwarding(unittest.TestCase):
|
||||
def test_forwards_bytes_both_directions(self):
|
||||
try:
|
||||
probe = socket.create_server(("127.0.0.16", 0))
|
||||
except OSError:
|
||||
self.skipTest("127.0.0.16 is not bound on this host")
|
||||
else:
|
||||
probe.close()
|
||||
_listener, target_port = _echo_server()
|
||||
handle = start_forwarder((
|
||||
ForwardSpec(
|
||||
label="egress",
|
||||
listen_host="127.0.0.16",
|
||||
listen_port=0,
|
||||
target_host="127.0.0.1",
|
||||
target_port=target_port,
|
||||
),
|
||||
))
|
||||
try:
|
||||
bound = handle.forwards[0]
|
||||
with socket.create_connection((bound.listen_host, bound.listen_port)) as sock:
|
||||
sock.sendall(b"hello")
|
||||
self.assertEqual(b"hello", sock.recv(5))
|
||||
finally:
|
||||
stop_forwarder(handle)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -432,12 +432,7 @@ class TestBundleLaunchSpec(unittest.TestCase):
|
||||
|
||||
def test_canary_env_visible_to_smolvm_guest(self):
|
||||
plan = _plan(canary=True)
|
||||
with patch.object(
|
||||
_launch._bundle,
|
||||
"bundle_host_port",
|
||||
return_value="65000",
|
||||
):
|
||||
stamped = _launch._discover_urls(plan, "127.0.0.16")
|
||||
stamped = _launch._discover_urls(plan, "127.0.0.16", {9099: 65000})
|
||||
|
||||
self.assertEqual(
|
||||
"fake-canary-value",
|
||||
@@ -507,21 +502,11 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
|
||||
|
||||
class TestProxyHost(unittest.TestCase):
|
||||
"""_proxy_host returns the bridge gateway on Linux and the loopback
|
||||
alias on other platforms."""
|
||||
"""_proxy_host returns the per-bottle address used by the forwarder."""
|
||||
|
||||
def test_linux_returns_bundle_gateway(self):
|
||||
def test_returns_loopback_alias(self):
|
||||
plan = _plan()
|
||||
with patch("bot_bottle.backend.smolmachines.launch.platform.system",
|
||||
return_value="Linux"):
|
||||
result = _launch._proxy_host(plan, "127.0.0.16")
|
||||
self.assertEqual(plan.bundle_gateway, result)
|
||||
|
||||
def test_non_linux_returns_loopback(self):
|
||||
plan = _plan()
|
||||
with patch("bot_bottle.backend.smolmachines.launch.platform.system",
|
||||
return_value="Darwin"):
|
||||
result = _launch._proxy_host(plan, "127.0.0.16")
|
||||
result = _launch._proxy_host(plan, "127.0.0.16")
|
||||
self.assertEqual("127.0.0.16", result)
|
||||
|
||||
|
||||
@@ -544,14 +529,20 @@ class TestDiscoverUrls(unittest.TestCase):
|
||||
),),
|
||||
),
|
||||
)
|
||||
with patch.object(_launch._bundle, "bundle_host_port", return_value="9420"):
|
||||
stamped = _launch._discover_urls(plan, "127.0.0.16")
|
||||
self.assertEqual("127.0.0.16:9420", stamped.agent_git_gate_host)
|
||||
stamped = _launch._discover_urls(
|
||||
plan,
|
||||
"127.0.0.16",
|
||||
{9099: 65000, 9420: 65001},
|
||||
)
|
||||
self.assertEqual("127.0.0.16:65001", stamped.agent_git_gate_host)
|
||||
|
||||
def test_supervise_url_set_when_supervise_present(self):
|
||||
plan = _plan(supervise=True)
|
||||
with patch.object(_launch._bundle, "bundle_host_port", return_value="55556"):
|
||||
stamped = _launch._discover_urls(plan, "127.0.0.16")
|
||||
stamped = _launch._discover_urls(
|
||||
plan,
|
||||
"127.0.0.16",
|
||||
{9099: 65000, 9100: 55556},
|
||||
)
|
||||
self.assertEqual("http://127.0.0.16:55556/", stamped.agent_supervise_url)
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ from bot_bottle.backend.smolmachines.sidecar_bundle import (
|
||||
bundle_network_name,
|
||||
create_bundle_network,
|
||||
ensure_bundle_image,
|
||||
start_bundle_vm,
|
||||
remove_bundle_network,
|
||||
start_bundle,
|
||||
stop_bundle,
|
||||
stop_bundle_vm,
|
||||
)
|
||||
|
||||
|
||||
@@ -200,6 +202,57 @@ class TestEnsureBundleImage(unittest.TestCase):
|
||||
self.assertEqual("Dockerfile.sidecars", kwargs["dockerfile"])
|
||||
|
||||
|
||||
class TestStartBundleVm(unittest.TestCase):
|
||||
def test_creates_sidecar_vm_with_ports_volumes_and_env(self):
|
||||
calls: list[tuple[str, tuple[object, ...], dict[str, object]]] = []
|
||||
|
||||
def record(name): # type: ignore
|
||||
def inner(*args, **kwargs): # type: ignore
|
||||
calls.append((name, args, kwargs))
|
||||
return inner
|
||||
|
||||
with patch(
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle.allocate_raw_host_ports",
|
||||
return_value={9099: 55001, 9420: 55002},
|
||||
), patch(
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_create",
|
||||
side_effect=record("create"),
|
||||
), patch(
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_start",
|
||||
side_effect=record("start"),
|
||||
), patch(
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.wait_exec_ready",
|
||||
side_effect=record("wait"),
|
||||
):
|
||||
launch = start_bundle_vm(
|
||||
_spec(
|
||||
daemons_csv="egress,git-http",
|
||||
environment=("FOO=bar", "TOKEN"),
|
||||
volumes=(("/host/routes", "/etc/egress", True),),
|
||||
ports_to_publish=(9099, 9420),
|
||||
),
|
||||
from_path=Path("/cache/sidecar.smolmachine"),
|
||||
host_env={"TOKEN": "secret"},
|
||||
)
|
||||
|
||||
self.assertEqual({9099: 55001, 9420: 55002}, launch.raw_ports)
|
||||
create = calls[0]
|
||||
self.assertEqual("create", create[0])
|
||||
self.assertEqual(("bot-bottle-sidecars-demo-abc12",), create[1])
|
||||
self.assertEqual(Path("/cache/sidecar.smolmachine"), create[2]["from_path"])
|
||||
self.assertTrue(create[2]["net"])
|
||||
self.assertEqual(((55001, 9099), (55002, 9420)), create[2]["ports"])
|
||||
self.assertEqual((("/host/routes", "/etc/egress", True),), create[2]["volumes"])
|
||||
self.assertEqual(
|
||||
{
|
||||
"BOT_BOTTLE_SIDECAR_DAEMONS": "egress,git-http",
|
||||
"FOO": "bar",
|
||||
"TOKEN": "secret",
|
||||
},
|
||||
create[2]["env"],
|
||||
)
|
||||
|
||||
|
||||
class TestStopBundle(unittest.TestCase):
|
||||
def _patch_run(self, **kwargs): # type: ignore
|
||||
return patch(
|
||||
@@ -222,5 +275,17 @@ class TestStopBundle(unittest.TestCase):
|
||||
stop_bundle("demo-abc12") # no raise
|
||||
|
||||
|
||||
class TestStopBundleVm(unittest.TestCase):
|
||||
def test_stops_then_deletes_sidecar_vm(self):
|
||||
with patch(
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_stop",
|
||||
) as stop, patch(
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_delete",
|
||||
) as delete:
|
||||
stop_bundle_vm("demo-abc12")
|
||||
stop.assert_called_once_with("bot-bottle-sidecars-demo-abc12")
|
||||
delete.assert_called_once_with("bot-bottle-sidecars-demo-abc12")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -106,6 +106,22 @@ class TestArgvShapes(unittest.TestCase):
|
||||
self.assertIn("--name", argv)
|
||||
self.assertIn("agent-xyz", argv)
|
||||
|
||||
def test_machine_create_with_net_ports_and_volumes(self):
|
||||
with self._patch_run() as m:
|
||||
machine_create(
|
||||
"sidecar-xyz",
|
||||
from_path=Path("/stage/sidecar.smolmachine"),
|
||||
net=True,
|
||||
ports=((55000, 9099),),
|
||||
volumes=(("/host/routes", "/etc/egress", True),),
|
||||
)
|
||||
argv = m.call_args.args[0]
|
||||
self.assertIn("--net", argv)
|
||||
self.assertIn("-p", argv)
|
||||
self.assertIn("55000:9099", argv)
|
||||
self.assertIn("-v", argv)
|
||||
self.assertIn("/host/routes:/etc/egress:ro", argv)
|
||||
|
||||
def test_machine_create_omits_net_when_no_allow_cidrs(self):
|
||||
with self._patch_run() as m:
|
||||
machine_create("agent-xyz", from_path=Path("/x.smolmachine"))
|
||||
|
||||
Reference in New Issue
Block a user