fix(sidecars): apply_routes_change targets the bundle + SIGHUP forwarding
Two bugs surfaced when applying an egress route change:
1. egress_apply.py still targeted claude-bottle-egress-<slug> —
the legacy per-sidecar container that no longer exists (it's
a docker-network alias on the bundle now). Switched it to
sidecar_bundle_container_name(slug), matching the chunk-5
fix already made to pipelock_apply.py.
2. `docker kill --signal HUP <bundle>` lands SIGHUP on the
supervisor (PID 1 in the bundle), which previously had no
SIGHUP handler — the signal was ignored. Added
`_Supervisor.forward_signal(sig, daemon_name)` and a SIGHUP
handler in main() that forwards to the egress daemon so
mitmdump's addon reload still works under the bundle.
Tests:
- New _Supervisor.forward_signal cases: forwards to the named
child (Python subprocess as the SIGHUP target — bash trap +
stdout=PIPE deferral interferes with the production-style
test); unknown-daemon name is a no-op.
Stale-reference cleanup (separate issue surfaced while looking
at this):
- claude_bottle/{egress,git_gate,egress_addon,
egress_addon_core,supervise_server}.py: Dockerfile.egress /
Dockerfile.git-gate / Dockerfile.supervise references updated
to Dockerfile.sidecars (the old per-sidecar Dockerfiles were
deleted in PRD 0024 chunk 5).
- tests/README.md: dropped the entry for
test_pipelock_sidecar_smoke (deleted in chunk 3) and added
the new bundle integration tests.
- git_gate.py: stale `DockerGitGate.start via docker cp`
reference (the method was deleted in chunk 3) rewritten to
the bind-mount path the renderer uses now.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+8
-6
@@ -40,15 +40,17 @@ Discovery is invoked with `-t .` (top-level dir = repo root) so the
|
||||
|
||||
## What the integration tests cover
|
||||
|
||||
- `test_pipelock_sidecar_smoke.py` — drives `DockerPipelockProxy.prepare`
|
||||
+ `.start` (the production code path) against a real Docker daemon and
|
||||
probes the sidecar's `/health` from an in-network curl container.
|
||||
- `test_dry_run_plan.py` — `cli.py start --dry-run --format=json` emits
|
||||
a structured plan that contains the resolved egress allowlist and
|
||||
the bottle's runtime, and creates zero Docker resources.
|
||||
- `test_orphan_cleanup.py` — `network_remove` and `PipelockProxy.stop`
|
||||
are idempotent against missing resources, so the EXIT trap can call
|
||||
them unconditionally.
|
||||
- `test_orphan_cleanup.py` — `network_remove` is idempotent against
|
||||
missing resources, so the EXIT trap can call it unconditionally.
|
||||
- `test_sidecar_bundle_image.py` — builds Dockerfile.sidecars and
|
||||
probes that pipelock / gitleaks / mitmdump / supervise are all
|
||||
reachable inside the bundle.
|
||||
- `test_sidecar_bundle_compose.py` — end-to-end compose-up of an
|
||||
agent + bundle pair; verifies the agent reaches the bundle via
|
||||
the legacy network aliases.
|
||||
|
||||
## Canaries
|
||||
|
||||
|
||||
@@ -174,6 +174,59 @@ class TestSupervisor(unittest.TestCase):
|
||||
self.assertEqual(2, rc)
|
||||
self.assertIsNone(sup.shutdown_at)
|
||||
|
||||
def test_forward_signal_to_named_child(self):
|
||||
# SIGHUP needs to reach mitmdump inside the bundle so
|
||||
# routes.yaml reloads (egress_apply.py issues `docker kill
|
||||
# --signal HUP <bundle>`). The supervisor forwards by daemon
|
||||
# name.
|
||||
#
|
||||
# The child is Python (not a shell-with-trap) on purpose:
|
||||
# bash on macOS defers trap execution while a foreground
|
||||
# builtin like `sleep` is running and stdout is a pipe,
|
||||
# which makes shell-trap test fixtures flaky. The
|
||||
# production code path (mitmdump as the bundle child)
|
||||
# doesn't have a shell in between — egress_entrypoint.sh
|
||||
# `exec`s mitmdump, so SIGHUP lands directly on mitmdump.
|
||||
sighup_marker = (
|
||||
sys.executable, "-c",
|
||||
"import signal, sys, time\n"
|
||||
"def _h(*_): sys.exit(42)\n"
|
||||
"signal.signal(signal.SIGHUP, _h)\n"
|
||||
"while True: time.sleep(0.1)\n",
|
||||
)
|
||||
specs = [
|
||||
_DaemonSpec("egress", sighup_marker),
|
||||
_DaemonSpec("other", ("/bin/sleep", "30")),
|
||||
]
|
||||
sup = _Supervisor(specs)
|
||||
sup.start_all()
|
||||
time.sleep(0.3) # let Python install the handler
|
||||
|
||||
delivered = sup.forward_signal(signal.SIGHUP, "egress")
|
||||
self.assertTrue(delivered)
|
||||
|
||||
deadline = time.monotonic() + 3.0
|
||||
while time.monotonic() < deadline:
|
||||
if sup.procs[0][1].poll() is not None:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
self.assertEqual(42, sup.procs[0][1].returncode,
|
||||
"egress did not see SIGHUP")
|
||||
# The other daemon is untouched.
|
||||
self.assertIsNone(sup.procs[1][1].poll())
|
||||
|
||||
sup.request_shutdown(reason="cleanup")
|
||||
self._drive(sup)
|
||||
|
||||
def test_forward_signal_unknown_daemon_no_op(self):
|
||||
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
|
||||
sup = _Supervisor(specs)
|
||||
sup.start_all()
|
||||
delivered = sup.forward_signal(signal.SIGHUP, "ghost")
|
||||
self.assertFalse(delivered)
|
||||
sup.request_shutdown(reason="cleanup")
|
||||
self._drive(sup)
|
||||
|
||||
def test_shutdown_after_start_terminates_children(self):
|
||||
# Two long-running children. Caller requests shutdown;
|
||||
# both should receive SIGTERM and exit. exit_code() is
|
||||
|
||||
Reference in New Issue
Block a user