3c0d2fb66f
Addresses review on #519 (@didericis 6143 + the fail-hard direction over the codex skip). Base owns the flow (6143 / ADR 0006). `attach_bottled_agents_to_gateway()` is now a concrete method on `BottleBackend` that delegates to `gateway_attach.reconcile_running_bottles`; backends override only three primitives — `_gateway_attach_resources()`, `_running_bottles()`, `_attach_bottle_to_gateway()`. The shared control flow + error policy live in one place so a backend can't drift onto a bespoke loop or a silent skip. Keeps the reconcile flow + `GatewayAttachResources` out of `backend/base.py` (the size guardrail) in a dedicated `backend/gateway_attach.py`. New ADR 0006 records the "shared behaviour in the base backend, subclasses override primitives" theme. Fail hard, never skip (the maintainer's direction over the codex review's skip). Any attach failure now aborts bring-up instead of being logged and skipped: a bottle that silently can't reach the fresh gateway (its egress just starts failing TLS) is worse than a loud failure. Every bottle is attempted and the failures are raised together (aggregate `InfraLaunchError`) so one bring-up surfaces the full blast radius. Resource gathering (the CA fetch) also fails hard. PRD 0081 goal + design updated to match (reversed from the earlier "tolerate per-bottle failures" draft). Bumps the base.py guardrail cap 580->600 for the new contract surface (the delegator + 3 abstract primitives); the flow itself lives in gateway_attach.py. Refs #516, #519. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.1 KiB
Python
55 lines
2.1 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."""
|
|
resources = backend._gateway_attach_resources()
|
|
failures: list[str] = []
|
|
for bottle in backend._running_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"]
|