fix(backend): reconcile reads the booted gateway's CA, not a default (0081)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import shutil
|
|||||||
import io
|
import io
|
||||||
from contextlib import contextmanager, redirect_stderr
|
from contextlib import contextmanager, redirect_stderr
|
||||||
from pathlib import Path
|
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 ...supervisor.types import SUPERVISE_HOSTNAME, SUPERVISE_PORT
|
||||||
from ...agent_provider import AgentProvisionPlan
|
from ...agent_provider import AgentProvisionPlan
|
||||||
@@ -41,12 +41,27 @@ from . import resolve_plan as _resolve_plan
|
|||||||
from .bottle import DockerBottle
|
from .bottle import DockerBottle
|
||||||
from .bottle_cleanup_plan import DockerBottleCleanupPlan
|
from .bottle_cleanup_plan import DockerBottleCleanupPlan
|
||||||
from .bottle_plan import DockerBottlePlan
|
from .bottle_plan import DockerBottlePlan
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .infra import DockerInfraService
|
||||||
|
|
||||||
|
|
||||||
class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanupPlan", str]):
|
class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanupPlan", str]):
|
||||||
"""Docker backend implementation. Selected by BOT_BOTTLE_BACKEND
|
"""Docker backend implementation. Selected by BOT_BOTTLE_BACKEND
|
||||||
when set to `docker`; retained as a legacy/example backend."""
|
when set to `docker`; retained as a legacy/example backend."""
|
||||||
|
|
||||||
name = "docker"
|
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
|
@classmethod
|
||||||
def is_available(cls) -> bool:
|
def is_available(cls) -> bool:
|
||||||
"""`docker` on PATH is sufficient; we don't probe `docker info`
|
"""`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:
|
def _gateway_attach_resources(self) -> GatewayAttachResources:
|
||||||
from .infra import DockerInfraService
|
from .infra import DockerInfraService
|
||||||
return GatewayAttachResources(
|
infra = self._infra if self._infra is not None else DockerInfraService()
|
||||||
ca_pem=DockerInfraService().gateway().ca_cert_pem())
|
return GatewayAttachResources(ca_pem=infra.gateway().ca_cert_pem())
|
||||||
|
|
||||||
def _running_bottles(self) -> Sequence[str]:
|
def _running_bottles(self) -> Sequence[str]:
|
||||||
from .infra_launch import running_agent_containers
|
from .infra_launch import running_agent_containers
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class DockerInfraService(InfraService):
|
|||||||
# untouched — no cold boot, nothing to reconcile.
|
# untouched — no cold boot, nothing to reconcile.
|
||||||
if cold_booted:
|
if cold_booted:
|
||||||
from .backend import DockerBottleBackend
|
from .backend import DockerBottleBackend
|
||||||
DockerBottleBackend().attach_bottled_agents_to_gateway()
|
DockerBottleBackend(infra=self).attach_bottled_agents_to_gateway()
|
||||||
return orchestrator.url()
|
return orchestrator.url()
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
|
|||||||
@@ -277,6 +277,27 @@ class TestDockerAttachPrimitives(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(docker.InfraLaunchError, "gone"):
|
with self.assertRaisesRegex(docker.InfraLaunchError, "gone"):
|
||||||
docker.push_ca_to_container("bot-bottle-a", "PEM")
|
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):
|
class TestMacosAttachPrimitives(unittest.TestCase):
|
||||||
def test_running_agent_containers_from_enumeration(self) -> None:
|
def test_running_agent_containers_from_enumeration(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user