feat(ssh-gate): wire gate into DockerBottlePlan, prepare, launch

PRD 0007: thread the DockerSSHGate through the bottle lifecycle.

- DockerBottlePlan gains gate_plan: SSHGatePlan.
- prepare.resolve_plan accepts a gate and renders its entrypoint
  script next to the pipelock yaml.
- launch.launch starts the gate sidecar after pipelock (so it's on
  the same internal + egress networks) and registers its stop in
  the ExitStack. Skipped when the bottle has no ssh entries.
- DockerBottleBackend instantiates DockerSSHGate alongside the
  pipelock proxy.
- bottle_plan.print + to_dict surface the upstream table so
  --dry-run shows the per-host listen-port mapping.

ssh_config provisioning still points at pipelock; that swap lands
in the next commit so this one stays a pure wiring change.
This commit is contained in:
2026-05-12 16:03:55 -04:00
parent c05d1ddcdb
commit 2533f8a00b
5 changed files with 46 additions and 2 deletions
+8 -2
View File
@@ -29,6 +29,7 @@ from .provision import git as _git
from .provision import prompt as _prompt
from .provision import skills as _skills
from .provision import ssh as _ssh
from .ssh_gate import DockerSSHGate
class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanupPlan"]):
@@ -39,13 +40,18 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
def __init__(self) -> None:
self._proxy = DockerPipelockProxy()
self._gate = DockerSSHGate()
def _resolve_plan(self, spec: BottleSpec, *, stage_dir: Path) -> DockerBottlePlan:
return _prepare.resolve_plan(spec, stage_dir=stage_dir, proxy=self._proxy)
return _prepare.resolve_plan(
spec, stage_dir=stage_dir, proxy=self._proxy, gate=self._gate
)
@contextmanager
def launch(self, plan: DockerBottlePlan) -> Generator[DockerBottle, None, None]:
with _launch.launch(plan, proxy=self._proxy, provision=self.provision) as bottle:
with _launch.launch(
plan, proxy=self._proxy, gate=self._gate, provision=self.provision
) as bottle:
yield bottle
def provision_ca(self, plan: DockerBottlePlan, target: str) -> None: