From 75957b75b8b99b7b123aa5fd5c754ad3558177b2 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 26 Jul 2026 22:52:47 -0400 Subject: [PATCH] fix(backend): reconcile reads the booted gateway's CA, not a default (0081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway-attach reconcile (PRD 0081) gathered its resources via a fresh default DockerInfraService(), so on a cold-boot bring-up of a NON-default gateway instance — e.g. an isolated integration test's `-itest-` gateway — it read the default `bot-bottle-orch-gateway`, which doesn't exist for that instance, and setUpClass died with "No such container: bot-bottle-orch-gateway" (the 0081 PR's integration-docker job). Thread the infra service whose gateway just cold-booted from DockerInfraService.ensure_running() into DockerBottleBackend so _gateway_attach_resources() reads THAT gateway's CA. Ordinary construction (no infra) still falls back to the per-host default singleton — production is unchanged — and the resources-first / fail-hard reconcile ordering is kept. Co-Authored-By: Claude Opus 4.8 --- bot_bottle/backend/docker/backend.py | 21 ++++++++++++++++--- bot_bottle/backend/docker/infra.py | 2 +- tests/unit/test_backend_secret_reprovision.py | 21 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) 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: