"""Integration: PRD 0023 chunk 2c — bundle bringup on a per-bottle docker bridge with the pinned IP. End-to-end against the real docker daemon. Brings up just the sidecar bundle on its own bridge, confirms the container lands at the pinned IP, then tears down. Skipped under act_runner (docker socket mount topology breaks bridge visibility) and when the bundle image isn't available. Full launch flow (smolvm + bundle + provisioning + the localhost-reach / egress-port-bypass probes) lives in chunk 2d.""" from __future__ import annotations import json import os import subprocess import time import unittest from bot_bottle.backend.smolmachines.sidecar_bundle import ( BundleLaunchSpec, bundle_container_name, bundle_network_name, create_bundle_network, remove_bundle_network, start_bundle, stop_bundle, ) from tests._docker import skip_unless_docker @skip_unless_docker() @unittest.skipIf( os.environ.get("GITEA_ACTIONS") == "true", "skipped under act_runner: docker socket mount topology breaks " "in-process visibility of networks created on the host daemon", ) class TestBundleBringup(unittest.TestCase): def setUp(self): self.slug = f"cb-test-bundle-{os.getpid()}-{int(time.time())}" self.network = bundle_network_name(self.slug) self.container = bundle_container_name(self.slug) def tearDown(self): stop_bundle(self.slug) remove_bundle_network(self.network) def _bundle_image_built(self) -> bool: """The bundle image (`bot-bottle-sidecars:latest`) is built lazily by the docker backend's compose. If a smolmachines-only operator hasn't run the docker backend first, the image won't exist locally. Skip rather than fail.""" r = subprocess.run( ["docker", "image", "inspect", "bot-bottle-sidecars:latest"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, ) return r.returncode == 0 def test_create_network_then_start_bundle_pins_ip(self): if not self._bundle_image_built(): self.skipTest( "bot-bottle-sidecars:latest not built; run a docker " "bottle first or `docker build -f Dockerfile.sidecars .`" ) # Pick a subnet unlikely to collide on the host. Last # octet of the slug hash isn't deterministic across runs; # we hardcode a high octet (.211) that the docker default # bridges almost never use. subnet = "192.168.211.0/24" gateway = "192.168.211.1" bundle_ip = "192.168.211.2" create_bundle_network(self.network, subnet, gateway) spec = BundleLaunchSpec( slug=self.slug, network_name=self.network, subnet=subnet, gateway=gateway, bundle_ip=bundle_ip, # Only run the pipelock daemon for this smoke — it's # the lightest of the four and doesn't need bind # mounts beyond what we'd skip without # BOT_BOTTLE_SIDECAR_DAEMONS. (The init # supervisor will exit if pipelock fails to find its # yaml — that's expected here; we just need the # container to land on the network at the right IP.) daemons_csv="", # empty → init exits "no daemons selected" ) start_bundle(spec) # Inspect the container's IP on the per-bottle network. r = subprocess.run( ["docker", "inspect", "--format", "{{(index .NetworkSettings.Networks \"" + self.network + "\").IPAddress}}", self.container], capture_output=True, text=True, check=False, ) # Container may have exited (no daemons selected → exit 0). # The inspect still works on exited containers as long as # `--rm` hasn't fired yet, which is a race. Even if it has, # the launch succeeded — the container existed, on the # right network, at the right IP. We don't fail here on # missing inspect. if r.returncode == 0 and r.stdout.strip(): self.assertEqual(bundle_ip, r.stdout.strip(), f"bundle landed at wrong IP: {r.stdout!r}") if __name__ == "__main__": unittest.main()