fix(macos-container): support git-gate launch
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 20s
lint / lint (push) Successful in 1m45s
prd-number / assign-numbers (push) Successful in 25s
test / unit (push) Successful in 32s
test / integration (push) Successful in 19s
Update Quality Badges / update-badges (push) Failing after 1m23s

This commit was merged in pull request #229.
This commit is contained in:
2026-06-10 21:53:22 -04:00
parent 932e71c0bf
commit bc9a22b46a
6 changed files with 280 additions and 24 deletions
@@ -44,3 +44,15 @@ class MacosContainerBottlePlan(BottlePlan):
@property
def agent_provider_template(self) -> str:
return self.agent_provision.template
@property
def git_gate_insteadof_host(self) -> str:
if self.agent_git_gate_url.startswith("http://"):
return self.agent_git_gate_url.removeprefix("http://").rstrip("/")
return super().git_gate_insteadof_host
@property
def git_gate_insteadof_scheme(self) -> str:
if self.agent_git_gate_url.startswith("http://"):
return "http"
return super().git_gate_insteadof_scheme
+77 -13
View File
@@ -23,7 +23,14 @@ from ...egress import EGRESS_ROUTES_IN_CONTAINER, egress_resolve_token_values
from ...git_gate import revoke_git_gate_provisioned_keys
from ...log import die, info, warn
from ...supervise import QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT
from ...util import expand_tilde
from ..docker.egress import EGRESS_CA_IN_CONTAINER, EGRESS_PORT
from ..docker.git_gate import (
GIT_GATE_ACCESS_HOOK_IN_CONTAINER,
GIT_GATE_CREDS_DIR_IN_CONTAINER,
GIT_GATE_ENTRYPOINT_IN_CONTAINER,
GIT_GATE_HOOK_IN_CONTAINER,
)
from ..docker.sidecar_bundle import (
SIDECAR_BUNDLE_DOCKERFILE,
SIDECAR_BUNDLE_IMAGE,
@@ -37,6 +44,8 @@ from .bottle_plan import MacosContainerBottlePlan
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
_AGENT_SLEEP_SECONDS = "2147483647"
_GIT_HTTP_PORT = 9420
_GIT_GATE_READY_FILE = "/run/git-gate/ready"
def internal_network_name(slug: str) -> str:
@@ -74,7 +83,6 @@ def launch(
raise teardown_exc
try:
_validate_supported_plan(plan)
plan = _mint_certs(plan)
_build_images(plan)
@@ -86,6 +94,7 @@ def launch(
container_mod.force_remove_container(sidecar_name)
_start_sidecar_bundle(plan, sidecar_name, internal_network, egress_network)
stack.callback(container_mod.force_remove_container, sidecar_name)
_stage_git_gate(plan, sidecar_name)
sidecar_ip = container_mod.container_ipv4_on_network(
sidecar_name, internal_network,
@@ -126,17 +135,6 @@ def _mint_certs(plan: MacosContainerBottlePlan) -> MacosContainerBottlePlan:
return dataclasses.replace(plan, egress_plan=egress_plan)
def _validate_supported_plan(plan: MacosContainerBottlePlan) -> None:
if plan.git_gate_plan.upstreams:
die(
"macos-container backend launch does not support bottle.git yet: "
"Apple Container cannot bind-mount individual SSH key files, "
"and this backend will not mount broad host key directories. "
"Use docker/smolmachines for git-gate bottles until a safe key "
"delivery path lands."
)
def _build_images(plan: MacosContainerBottlePlan) -> None:
container_mod.build_image(
SIDECAR_BUNDLE_IMAGE,
@@ -213,14 +211,76 @@ def _stamp_agent_urls(
supervise_url = ""
if plan.supervise_plan is not None:
supervise_url = f"http://{sidecar_ip}:{SUPERVISE_PORT}/"
git_gate_url = ""
if plan.git_gate_plan.upstreams:
git_gate_url = f"http://{sidecar_ip}:{_GIT_HTTP_PORT}"
return dataclasses.replace(
plan,
agent_proxy_url=proxy_url,
agent_git_gate_url="",
agent_git_gate_url=git_gate_url,
agent_supervise_url=supervise_url,
)
def _stage_git_gate(plan: MacosContainerBottlePlan, sidecar_name: str) -> None:
gp = plan.git_gate_plan
if not gp.upstreams:
return
container_mod.exec_container(
sidecar_name,
[
"mkdir",
"-p",
str(Path(GIT_GATE_HOOK_IN_CONTAINER).parent),
GIT_GATE_CREDS_DIR_IN_CONTAINER,
"/git",
str(Path(_GIT_GATE_READY_FILE).parent),
],
)
for host_path, container_path in _git_gate_files(plan):
container_mod.copy_into_container(
sidecar_name, host_path, container_path,
)
container_mod.exec_container(
sidecar_name,
[
"sh",
"-c",
"chmod 755 "
f"{GIT_GATE_ENTRYPOINT_IN_CONTAINER} "
f"{GIT_GATE_HOOK_IN_CONTAINER} "
f"{GIT_GATE_ACCESS_HOOK_IN_CONTAINER} && "
f"chmod 600 {GIT_GATE_CREDS_DIR_IN_CONTAINER}/* && "
f"touch {_GIT_GATE_READY_FILE}",
],
)
def _git_gate_files(
plan: MacosContainerBottlePlan,
) -> tuple[tuple[str, str], ...]:
gp = plan.git_gate_plan
files: list[tuple[str, str]] = [
(str(gp.entrypoint_script), GIT_GATE_ENTRYPOINT_IN_CONTAINER),
(str(gp.hook_script), GIT_GATE_HOOK_IN_CONTAINER),
(str(gp.access_hook_script), GIT_GATE_ACCESS_HOOK_IN_CONTAINER),
]
for upstream in gp.upstreams:
files.append((
expand_tilde(upstream.identity_file),
f"{GIT_GATE_CREDS_DIR_IN_CONTAINER}/{upstream.name}-key",
))
if upstream.known_hosts_file:
files.append((
str(upstream.known_hosts_file),
f"{GIT_GATE_CREDS_DIR_IN_CONTAINER}/{upstream.name}-known_hosts",
))
return tuple(files)
def _sidecar_run_argv(
plan: MacosContainerBottlePlan,
sidecar_name: str,
@@ -269,6 +329,8 @@ def _sidecar_dns() -> str:
def _sidecar_daemons(plan: MacosContainerBottlePlan) -> tuple[str, ...]:
daemons = ["egress"]
if plan.git_gate_plan.upstreams:
daemons += ["git-gate", "git-http"]
if plan.supervise_plan is not None:
daemons.append("supervise")
return tuple(daemons)
@@ -278,6 +340,8 @@ def _sidecar_env_entries(plan: MacosContainerBottlePlan) -> tuple[str, ...]:
env: list[str] = []
if plan.egress_plan.routes:
env.extend(sorted(plan.egress_plan.token_env_map.keys()))
if plan.git_gate_plan.upstreams:
env.append(f"BOT_BOTTLE_GIT_GATE_READY_FILE={_GIT_GATE_READY_FILE}")
if plan.supervise_plan is not None:
env += [
f"SUPERVISE_BOTTLE_SLUG={plan.slug}",
@@ -8,6 +8,7 @@ import ipaddress
import platform
import shutil
import subprocess
import time
from typing import Iterable
from ...log import die, info
@@ -213,6 +214,45 @@ def force_remove_container(name: str) -> None:
)
def copy_into_container(name: str, host_path: str, container_path: str) -> None:
cmd = [_CONTAINER, "cp", host_path, f"{name}:{container_path}"]
result = _run_container_op(cmd)
if result.returncode != 0:
die(
f"container cp into {name}:{container_path} failed: "
f"{(result.stderr or '').strip() or '<no stderr>'}"
)
def exec_container(name: str, argv: list[str]) -> None:
result = _run_container_op([_CONTAINER, "exec", name, *argv])
if result.returncode != 0:
die(
f"container exec in {name} failed: "
f"{(result.stderr or '').strip() or '<no stderr>'}"
)
def _run_container_op(cmd: list[str]) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False,
)
for _ in range(19):
if result.returncode == 0:
return result
time.sleep(0.1)
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False,
)
return result
def create_network(name: str, *, internal: bool = False) -> None:
args = [
_CONTAINER, "network", "create",
+20 -2
View File
@@ -59,6 +59,7 @@ class _DaemonSpec:
# reads to inject `Authorization` headers on configured routes;
# no other daemon in the bundle should see these values.
_EGRESS_ONLY_ENV_PREFIXES: tuple[str, ...] = ("EGRESS_TOKEN_",)
_READY_GATED_DAEMONS: tuple[str, ...] = ("git-gate", "git-http")
def _env_for_daemon(name: str, base_env: dict[str, str]) -> dict[str, str]:
@@ -82,6 +83,22 @@ _DAEMONS: tuple[_DaemonSpec, ...] = (
)
def _argv_for_daemon(name: str, argv: Sequence[str], env: dict[str, str]) -> list[str]:
ready_file = env.get("BOT_BOTTLE_GIT_GATE_READY_FILE", "").strip()
if name not in _READY_GATED_DAEMONS or not ready_file:
return list(argv)
return [
"/bin/sh",
"-c",
"while [ ! -f \"$BOT_BOTTLE_GIT_GATE_READY_FILE\" ]; do "
"sleep 0.1; "
"done; "
"exec \"$@\"",
name,
*argv,
]
def _selected_daemons(
env: dict[str, str],
all_daemons: Sequence[_DaemonSpec] | None = None,
@@ -118,12 +135,13 @@ def _pump(name: str, stream: IO[bytes]) -> None:
def _spawn(spec: _DaemonSpec) -> subprocess.Popen[bytes]:
env = _env_for_daemon(spec.name, dict(os.environ))
proc = subprocess.Popen(
list(spec.argv),
_argv_for_daemon(spec.name, spec.argv, env),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
env=_env_for_daemon(spec.name, dict(os.environ)),
env=env,
)
threading.Thread(
target=_pump, args=(spec.name, proc.stdout), daemon=True
+108 -9
View File
@@ -34,16 +34,26 @@ def _plan(
token_env_map={"EGRESS_TOKEN_0": "HOST_TOKEN"},
)
if git:
key_path = stage_dir / "origin-key"
key_path.write_text("key\n", encoding="utf-8")
known_hosts_path = stage_dir / "origin-known-hosts"
known_hosts_path.write_text("example.com ssh-ed25519 AAAA\n", encoding="utf-8")
entrypoint = stage_dir / "git_gate_entrypoint.sh"
entrypoint.write_text("#!/bin/sh\n", encoding="utf-8")
hook = stage_dir / "git_gate_pre_receive.sh"
hook.write_text("#!/bin/sh\n", encoding="utf-8")
access_hook = stage_dir / "git_gate_access_hook.sh"
access_hook.write_text("#!/bin/sh\n", encoding="utf-8")
upstream = SimpleNamespace(
name="origin",
identity_file="/host/key",
known_hosts_file=Path("/host/known_hosts"),
identity_file=str(key_path),
known_hosts_file=known_hosts_path,
)
git_gate_plan = SimpleNamespace(
upstreams=(upstream,),
entrypoint_script=Path("/state/git/entrypoint.sh"),
hook_script=Path("/state/git/pre-receive"),
access_hook_script=Path("/state/git/access.sh"),
entrypoint_script=entrypoint,
hook_script=hook,
access_hook_script=access_hook,
)
else:
git_gate_plan = SimpleNamespace(upstreams=())
@@ -56,6 +66,7 @@ def _plan(
provisioned_env={"CODEX_HOME": "/run/codex-home"},
)
return cast(MacosContainerBottlePlan, SimpleNamespace(
spec=SimpleNamespace(),
stage_dir=stage_dir,
slug="dev-abc",
container_name="bot-bottle-dev-abc",
@@ -152,11 +163,99 @@ class TestMacosContainerLaunchArgv(unittest.TestCase):
self.assertNotIn("bot-bottle-egress-dev-abc", argv)
self.assertEqual(["bot-bottle-agent:latest", "sleep", "2147483647"], argv[-3:])
def test_git_gate_is_blocked_until_safe_key_delivery_exists(self):
def test_git_gate_daemons_are_ready_gated(self):
plan = _plan(stage_dir=self.stage_dir, git=True)
with patch.object(launch, "die", side_effect=SystemExit("die")):
with self.assertRaises(SystemExit):
launch._validate_supported_plan(plan)
self.assertEqual(
("egress", "git-gate", "git-http"),
launch._sidecar_daemons(plan),
)
self.assertIn(
"BOT_BOTTLE_GIT_GATE_READY_FILE=/run/git-gate/ready",
launch._sidecar_env_entries(plan),
)
def test_stamp_agent_urls_includes_git_http_when_git_gate_exists(self):
plan = _plan(stage_dir=self.stage_dir, git=True, supervise=True)
with patch.object(launch.dataclasses, "replace") as replace:
launch._stamp_agent_urls(plan, "192.168.128.2")
replace.assert_called_once_with(
plan,
agent_proxy_url="http://192.168.128.2:9099",
agent_git_gate_url="http://192.168.128.2:9420",
agent_supervise_url="http://192.168.128.2:9100/",
)
def test_macos_plan_uses_http_git_gate_rewrites(self):
base = _plan(
stage_dir=self.stage_dir,
git=True,
agent_git_gate_url="http://192.168.128.2:9420",
)
plan = MacosContainerBottlePlan(
spec=base.spec,
stage_dir=base.stage_dir,
git_gate_plan=base.git_gate_plan,
egress_plan=base.egress_plan,
supervise_plan=base.supervise_plan,
agent_provision=base.agent_provision,
slug=base.slug,
forwarded_env=base.forwarded_env,
agent_git_gate_url=base.agent_git_gate_url,
)
self.assertEqual(
"192.168.128.2:9420",
plan.git_gate_insteadof_host,
)
self.assertEqual("http", plan.git_gate_insteadof_scheme)
def test_stage_git_gate_copies_files_and_releases_ready_marker(self):
plan = _plan(stage_dir=self.stage_dir, git=True)
with (
patch.object(launch.container_mod, "exec_container") as exec_container,
patch.object(launch.container_mod, "copy_into_container") as copy_in,
):
launch._stage_git_gate(plan, "sidecar")
exec_container.assert_any_call(
"sidecar",
[
"mkdir",
"-p",
"/etc/git-gate",
"/git-gate/creds",
"/git",
"/run/git-gate",
],
)
copied = [call.args for call in copy_in.call_args_list]
self.assertIn(
(
"sidecar",
str(self.stage_dir / "git_gate_entrypoint.sh"),
"/git-gate-entrypoint.sh",
),
copied,
)
self.assertIn(
(
"sidecar",
str(self.stage_dir / "origin-key"),
"/git-gate/creds/origin-key",
),
copied,
)
self.assertIn(
(
"sidecar",
str(self.stage_dir / "origin-known-hosts"),
"/git-gate/creds/origin-known_hosts",
),
copied,
)
self.assertIn(
"touch /run/git-gate/ready",
exec_container.call_args_list[-1].args[1][-1],
)
if __name__ == "__main__":
+23
View File
@@ -21,6 +21,7 @@ from unittest.mock import patch
from bot_bottle.sidecar_init import (
_DaemonSpec,
_Supervisor,
_argv_for_daemon,
_env_for_daemon,
_selected_daemons,
)
@@ -120,6 +121,28 @@ class TestSelectedDaemons(unittest.TestCase):
self.assertEqual([d.name for d in got], ["egress", "git-gate"])
class TestDaemonArgv(unittest.TestCase):
def test_git_daemons_wait_for_ready_marker_when_configured(self):
argv = _argv_for_daemon(
"git-gate",
("/bin/sh", "/git-gate-entrypoint.sh"),
{"BOT_BOTTLE_GIT_GATE_READY_FILE": "/run/git-gate/ready"},
)
self.assertEqual("/bin/sh", argv[0])
self.assertEqual("-c", argv[1])
self.assertIn("BOT_BOTTLE_GIT_GATE_READY_FILE", argv[2])
self.assertEqual("git-gate", argv[3])
self.assertEqual(["/bin/sh", "/git-gate-entrypoint.sh"], argv[4:])
def test_non_git_daemon_does_not_wait_for_ready_marker(self):
argv = _argv_for_daemon(
"egress",
("/bin/sh", "/app/egress-entrypoint.sh"),
{"BOT_BOTTLE_GIT_GATE_READY_FILE": "/run/git-gate/ready"},
)
self.assertEqual(["/bin/sh", "/app/egress-entrypoint.sh"], argv)
class TestSupervisor(unittest.TestCase):
"""End-to-end: drive `_Supervisor` directly with fake commands.
We don't go through `main()` because main installs signal