Files
bot-bottle/bot_bottle/backend/gateway_attach.py
T
didericis b315a127b8
tracker-policy-pr / check-pr (pull_request) Successful in 12s
lint / lint (push) Successful in 56s
test / unit (pull_request) Failing after 46s
test / integration-docker (pull_request) Successful in 1m1s
test / coverage (pull_request) Has been skipped
fix(backend): skip gateway reconcile when no running bottles (0081)
reconcile_running_bottles() gathered the gateway attach-resources (a read of
the per-host singleton gateway's CA) *before* enumerating running bottles. On
a cold-boot bring-up of a NON-default gateway instance with no host bottles —
e.g. an isolated integration test's `-itest-` gateway — that read targeted the
default `bot-bottle-orch-gateway`, which doesn't exist for that instance, so
setUpClass died with "No such container: bot-bottle-orch-gateway" (seen on the
0081 PR's integration-docker job).

Enumerate running bottles first and return early when there are none: the
reconcile is a no-op with nothing to reattach, so the CA read is both wasted
and wrong for a non-default instance. Production (default names, real bottles)
is unchanged. Adds unit coverage for the three reconcile paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:52:47 -04:00

64 lines
2.6 KiB
Python

"""The shared gateway-attach reconcile flow (PRD 0081).
`BottleBackend.attach_bottled_agents_to_gateway()` delegates here so every
backend reconciles running bottles against a freshly-booted gateway *the same
way* — the control flow + fail-hard policy live in one place and backends
override only the primitives (see ADR 0006). Kept out of `backend/base.py` so
the backend contract module stays lean (the base.py size guardrail).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from .base import InfraLaunchError
if TYPE_CHECKING:
from .base import BottleBackend
@dataclass(frozen=True)
class GatewayAttachResources:
"""Everything a running bottle needs to (re)attach to the current gateway on
a cold boot (PRD 0081). Gathered once per reconcile and handed to every
bottle. The CA now; egress secrets and git-gate state join as the reconcile
grows (#516)."""
ca_pem: str
def reconcile_running_bottles(backend: "BottleBackend[Any, Any, Any]") -> None:
"""Reconcile every already-running bottle against the freshly-(re)booted
gateway: gather the attach resources once, then attach every live bottle.
Fail hard, never skip: a bottle that silently can't reach the fresh gateway
(its egress just starts failing TLS) is worse than a loud bring-up failure,
so any attach failure aborts bring-up. Every bottle is attempted first and
the failures are raised together, so one bring-up surfaces the full blast
radius rather than one bottle at a time."""
# Enumerate the host's running bottles BEFORE gathering resources: with none
# to reattach, the reconcile is a no-op, and gathering resources first would
# read the current gateway's CA for nothing. That read targets the per-host
# singleton gateway, so a bring-up of a *non-default* instance with no host
# bottles (an isolated integration test's `-itest-` gateway) would otherwise
# fail looking for the default `bot-bottle-orch-gateway` (#519 review).
bottles = backend._running_bottles()
if not bottles:
return
resources = backend._gateway_attach_resources()
failures: list[str] = []
for bottle in bottles:
try:
backend._attach_bottle_to_gateway(bottle, resources)
except InfraLaunchError as exc:
failures.append(str(exc))
if failures:
raise InfraLaunchError(
f"could not attach {len(failures)} running bottle(s) to the "
f"freshly-booted gateway:\n " + "\n ".join(failures)
)
__all__ = ["GatewayAttachResources", "reconcile_running_bottles"]