diff --git a/bot_bottle/backend/docker/backend.py b/bot_bottle/backend/docker/backend.py index 83e61815..b332d594 100644 --- a/bot_bottle/backend/docker/backend.py +++ b/bot_bottle/backend/docker/backend.py @@ -23,7 +23,7 @@ import shutil import io from contextlib import contextmanager, redirect_stderr from pathlib import Path -from typing import Generator, Sequence +from typing import TYPE_CHECKING, Generator, Sequence from ...supervisor.types import SUPERVISE_HOSTNAME, SUPERVISE_PORT from ...agent_provider import AgentProvisionPlan @@ -41,12 +41,27 @@ from . import resolve_plan as _resolve_plan from .bottle import DockerBottle from .bottle_cleanup_plan import DockerBottleCleanupPlan from .bottle_plan import DockerBottlePlan + +if TYPE_CHECKING: + from .infra import DockerInfraService + + class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanupPlan", str]): """Docker backend implementation. Selected by BOT_BOTTLE_BACKEND when set to `docker`; retained as a legacy/example backend.""" name = "docker" + def __init__(self, *, infra: "DockerInfraService | None" = None) -> None: + # The infra service whose gateway just cold-booted, passed in on the + # bring-up reconcile path (PRD 0081) so `_gateway_attach_resources` + # reads THAT gateway's CA rather than a fresh default-named service — + # otherwise a non-default instance (an isolated integration test's + # `-itest-` gateway) reads the default `bot-bottle-orch-gateway`, which + # doesn't exist for it (#519 review). None for ordinary construction: + # falls back to the per-host default singleton. + self._infra = infra + @classmethod def is_available(cls) -> bool: """`docker` on PATH is sufficient; we don't probe `docker info` @@ -144,8 +159,8 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup def _gateway_attach_resources(self) -> GatewayAttachResources: from .infra import DockerInfraService - return GatewayAttachResources( - ca_pem=DockerInfraService().gateway().ca_cert_pem()) + infra = self._infra if self._infra is not None else DockerInfraService() + return GatewayAttachResources(ca_pem=infra.gateway().ca_cert_pem()) def _running_bottles(self) -> Sequence[str]: from .infra_launch import running_agent_containers diff --git a/bot_bottle/backend/docker/infra.py b/bot_bottle/backend/docker/infra.py index d3808953..b2c3f666 100644 --- a/bot_bottle/backend/docker/infra.py +++ b/bot_bottle/backend/docker/infra.py @@ -151,7 +151,7 @@ class DockerInfraService(InfraService): # untouched — no cold boot, nothing to reconcile. if cold_booted: from .backend import DockerBottleBackend - DockerBottleBackend().attach_bottled_agents_to_gateway() + DockerBottleBackend(infra=self).attach_bottled_agents_to_gateway() return orchestrator.url() def stop(self) -> None: diff --git a/tests/unit/test_backend_secret_reprovision.py b/tests/unit/test_backend_secret_reprovision.py index 36592faa..60671cb0 100644 --- a/tests/unit/test_backend_secret_reprovision.py +++ b/tests/unit/test_backend_secret_reprovision.py @@ -277,6 +277,27 @@ class TestDockerAttachPrimitives(unittest.TestCase): with self.assertRaisesRegex(docker.InfraLaunchError, "gone"): docker.push_ca_to_container("bot-bottle-a", "PEM") + def test_gateway_attach_resources_reads_the_threaded_infra_gateway(self) -> None: + # On the bring-up reconcile path the backend is threaded with the infra + # whose gateway just cold-booted, so it reads THAT gateway's CA — not a + # fresh default-named DockerInfraService that wouldn't exist for a + # non-default instance (#519 review). + from bot_bottle.backend.docker.backend import DockerBottleBackend + infra = Mock() + infra.gateway.return_value.ca_cert_pem.return_value = "ITEST-CA" + res = DockerBottleBackend(infra=infra)._gateway_attach_resources() + self.assertEqual("ITEST-CA", res.ca_pem) + infra.gateway.assert_called_once() + + def test_gateway_attach_resources_defaults_to_the_host_singleton(self) -> None: + # Ordinary construction (no infra) falls back to the per-host default. + from bot_bottle.backend.docker.backend import DockerBottleBackend + with patch("bot_bottle.backend.docker.infra.DockerInfraService") as InfraCls: + InfraCls.return_value.gateway.return_value.ca_cert_pem.return_value = "DEFAULT-CA" + res = DockerBottleBackend()._gateway_attach_resources() + InfraCls.assert_called_once_with() + self.assertEqual("DEFAULT-CA", res.ca_pem) + class TestMacosAttachPrimitives(unittest.TestCase): def test_running_agent_containers_from_enumeration(self) -> None: