refactor(gateway): each backend's transport in its own gateway_transport.py
Give every backend a `gateway_transport.py` holding just its `GatewayTransport` impl, named consistently with the backend: `DockerGatewayTransport`, `MacosGatewayTransport` (was `AppleGatewayTransport`), and `FirecrackerGatewayTransport` (was `SshGatewayTransport`). - docker: split `DockerGatewayTransport` out of `gateway_provision.py`, which keeps only the backend-neutral provisioning logic (provision_git_gate / deprovision_git_gate) the other backends share. - macOS: `gateway_provision.py` held only the transport, so it's replaced by `gateway_transport.py`. - firecracker: move the transport out of `gateway.py`. Behaviour-preserving; importers + tests updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||
from ...orchestrator.reprovision import reprovision_bottles
|
||||
from ..consolidated_util import provision_bottle
|
||||
from ..consolidated_util import teardown_consolidated as _teardown_util
|
||||
from .gateway_provision import DockerGatewayTransport
|
||||
from .gateway_transport import DockerGatewayTransport
|
||||
from .gateway_net import next_free_ip
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
from .util import run_docker
|
||||
from .gateway_provision import DockerGatewayTransport
|
||||
from .gateway_transport import DockerGatewayTransport
|
||||
from ...paths import (
|
||||
ORCHESTRATOR_AUTH_JWT_ENV,
|
||||
host_gateway_ca_dir,
|
||||
|
||||
@@ -16,7 +16,6 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from .util import run_docker
|
||||
from ...git_gate import GitGatePlan, git_gate_render_provision
|
||||
from ...gateway import GatewayProvisionError, GatewayTransport
|
||||
|
||||
@@ -27,27 +26,6 @@ from ...gateway import GatewayProvisionError, GatewayTransport
|
||||
_SAFE_BOTTLE_ID = re.compile(r"[A-Za-z0-9_-]+")
|
||||
|
||||
|
||||
class DockerGatewayTransport:
|
||||
"""`GatewayTransport` for the docker gateway container (exec/cp)."""
|
||||
|
||||
def __init__(self, gateway: str) -> None:
|
||||
self.gateway = gateway
|
||||
|
||||
def exec(self, argv: list[str]) -> None:
|
||||
proc = run_docker(["docker", "exec", self.gateway, *argv])
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"gateway exec {argv!r} failed: {proc.stderr.strip()}"
|
||||
)
|
||||
|
||||
def cp_into(self, src: str, dest: str) -> None:
|
||||
proc = run_docker(["docker", "cp", src, f"{self.gateway}:{dest}"])
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"gateway cp {src} -> {dest} failed: {proc.stderr.strip()}"
|
||||
)
|
||||
|
||||
|
||||
def _require_safe(bottle_id: str) -> None:
|
||||
if not _SAFE_BOTTLE_ID.fullmatch(bottle_id):
|
||||
raise GatewayProvisionError(f"unsafe bottle id {bottle_id!r}")
|
||||
@@ -109,5 +87,5 @@ def deprovision_git_gate(transport: GatewayTransport, bottle_id: str) -> None:
|
||||
|
||||
__all__ = [
|
||||
"provision_git_gate", "deprovision_git_gate",
|
||||
"GatewayProvisionError", "GatewayTransport", "DockerGatewayTransport",
|
||||
"GatewayProvisionError", "GatewayTransport",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""The `GatewayTransport` for the docker gateway container (PRD 0070).
|
||||
|
||||
How the launcher stages files + runs commands in the running gateway container:
|
||||
`docker exec` / `docker cp` over the docker socket. The backend-neutral
|
||||
provisioning logic that drives it lives in `gateway_provision`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .util import run_docker
|
||||
from ...gateway import GatewayProvisionError
|
||||
|
||||
|
||||
class DockerGatewayTransport:
|
||||
"""`GatewayTransport` for the docker gateway container (exec/cp)."""
|
||||
|
||||
def __init__(self, gateway: str) -> None:
|
||||
self.gateway = gateway
|
||||
|
||||
def exec(self, argv: list[str]) -> None:
|
||||
proc = run_docker(["docker", "exec", self.gateway, *argv])
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"gateway exec {argv!r} failed: {proc.stderr.strip()}"
|
||||
)
|
||||
|
||||
def cp_into(self, src: str, dest: str) -> None:
|
||||
proc = run_docker(["docker", "cp", src, f"{self.gateway}:{dest}"])
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"gateway cp {src} -> {dest} failed: {proc.stderr.strip()}"
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["DockerGatewayTransport"]
|
||||
@@ -20,22 +20,18 @@ Orchestrator service lands and `infra_vm` is dissolved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ...gateway import (
|
||||
DEFAULT_CA_TIMEOUT_SECONDS,
|
||||
Gateway,
|
||||
GatewayError,
|
||||
GatewayProvisionError,
|
||||
GatewayTransport,
|
||||
)
|
||||
from .. import util as backend_util
|
||||
from . import infra_vm, netpool, util
|
||||
from .gateway_transport import FirecrackerGatewayTransport
|
||||
|
||||
# The gateway microVM's name (the `bb_role=gateway` boot tag / run dir). Fixed
|
||||
# per host — one gateway VM, shared by every agent VM.
|
||||
@@ -57,40 +53,6 @@ _GATEWAY_CA_PATH = "/home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem"
|
||||
_CA_FETCH_TIMEOUT_SECONDS = 15.0
|
||||
|
||||
|
||||
class SshGatewayTransport:
|
||||
"""`GatewayTransport` for the gateway running in the gateway VM — the docker
|
||||
exec/cp equivalents over SSH (dropbear + the stable infra key)."""
|
||||
|
||||
def __init__(self, private_key: Path, guest_ip: str) -> None:
|
||||
self._key = private_key
|
||||
self._ip = guest_ip
|
||||
|
||||
def exec(self, argv: list[str]) -> None:
|
||||
proc = subprocess.run(
|
||||
util.ssh_base_argv(self._key, self._ip) + [shlex.join(argv)],
|
||||
capture_output=True, text=True, timeout=60, check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"infra gateway exec {argv!r} failed: {proc.stderr.strip()}")
|
||||
|
||||
def cp_into(self, src: str, dest: str) -> None:
|
||||
# Preserve the source mode (docker cp does): the access-hook is staged
|
||||
# 0700 and git-http execs it directly — a plain `cat >` would land it
|
||||
# 0644 and the exec fails with EACCES; keys stay 0600.
|
||||
mode = stat.S_IMODE(os.stat(src).st_mode)
|
||||
q = shlex.quote(dest)
|
||||
proc = subprocess.run(
|
||||
util.ssh_base_argv(self._key, self._ip)
|
||||
+ [f"cat > {q} && chmod {mode:o} {q}"],
|
||||
input=Path(src).read_bytes(), capture_output=True, timeout=30, check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"infra gateway cp {src} -> {dest} failed: "
|
||||
f"{proc.stderr.decode(errors='replace').strip()}")
|
||||
|
||||
|
||||
class FirecrackerGateway(Gateway):
|
||||
"""The consolidated gateway as a Firecracker microVM on the gateway link.
|
||||
|
||||
@@ -188,8 +150,8 @@ class FirecrackerGateway(Gateway):
|
||||
deploy keys through (over SSH to the gateway VM). Needs no live handle —
|
||||
built from the stable key + the gateway link's guest IP, so teardown can
|
||||
use it too."""
|
||||
return SshGatewayTransport(
|
||||
return FirecrackerGatewayTransport(
|
||||
infra_vm._infra_dir() / "id_ed25519", netpool.gw_slot().guest_ip)
|
||||
|
||||
|
||||
__all__ = ["FirecrackerGateway", "SshGatewayTransport", "GATEWAY_NAME"]
|
||||
__all__ = ["FirecrackerGateway", "FirecrackerGatewayTransport", "GATEWAY_NAME"]
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""The `GatewayTransport` for the gateway running in the gateway microVM
|
||||
(PRD 0070).
|
||||
|
||||
How the launcher stages files + runs commands in the running gateway: the
|
||||
docker exec/cp equivalents over SSH (dropbear + the stable infra key). The
|
||||
backend-neutral provisioning logic that drives it lives in
|
||||
`backend.docker.gateway_provision`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from ...gateway import GatewayProvisionError
|
||||
from . import util
|
||||
|
||||
|
||||
class FirecrackerGatewayTransport:
|
||||
"""`GatewayTransport` for the gateway running in the gateway VM — the docker
|
||||
exec/cp equivalents over SSH (dropbear + the stable infra key)."""
|
||||
|
||||
def __init__(self, private_key: Path, guest_ip: str) -> None:
|
||||
self._key = private_key
|
||||
self._ip = guest_ip
|
||||
|
||||
def exec(self, argv: list[str]) -> None:
|
||||
proc = subprocess.run(
|
||||
util.ssh_base_argv(self._key, self._ip) + [shlex.join(argv)],
|
||||
capture_output=True, text=True, timeout=60, check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"infra gateway exec {argv!r} failed: {proc.stderr.strip()}")
|
||||
|
||||
def cp_into(self, src: str, dest: str) -> None:
|
||||
# Preserve the source mode (docker cp does): the access-hook is staged
|
||||
# 0700 and git-http execs it directly — a plain `cat >` would land it
|
||||
# 0644 and the exec fails with EACCES; keys stay 0600.
|
||||
mode = stat.S_IMODE(os.stat(src).st_mode)
|
||||
q = shlex.quote(dest)
|
||||
proc = subprocess.run(
|
||||
util.ssh_base_argv(self._key, self._ip)
|
||||
+ [f"cat > {q} && chmod {mode:o} {q}"],
|
||||
input=Path(src).read_bytes(), capture_output=True, timeout=30, check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise GatewayProvisionError(
|
||||
f"infra gateway cp {src} -> {dest} failed: "
|
||||
f"{proc.stderr.decode(errors='replace').strip()}")
|
||||
|
||||
|
||||
__all__ = ["FirecrackerGatewayTransport"]
|
||||
@@ -47,7 +47,7 @@ from ..consolidated_util import (
|
||||
from . import util as container_mod
|
||||
from .enumerate import CONTAINER_NAME_PREFIX, EnumerationError, enumerate_active
|
||||
from .gateway import GATEWAY_NETWORK
|
||||
from .gateway_provision import AppleGatewayTransport
|
||||
from .gateway_transport import MacosGatewayTransport
|
||||
from .infra import MacosInfraService, OrchestratorStartError
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ def register_agent(
|
||||
except (OrchestratorClientError, EnumerationError) as e:
|
||||
info(f"registry reconciliation skipped: {e}")
|
||||
reg = provision_bottle(
|
||||
client, source_ip, egress_plan, git_gate_plan, AppleGatewayTransport(),
|
||||
client, source_ip, egress_plan, git_gate_plan, MacosGatewayTransport(),
|
||||
image_ref=image_ref, tokens=tokens,
|
||||
env_var_secret=env_var_secret,
|
||||
)
|
||||
@@ -193,7 +193,7 @@ def teardown_consolidated(
|
||||
"""Deregister the bottle and remove its git-gate state from the gateway.
|
||||
Both steps are idempotent so this is safe from a cleanup trap. Does NOT
|
||||
stop the gateway — it's a persistent per-host singleton."""
|
||||
_teardown_util(bottle_id, AppleGatewayTransport(),
|
||||
_teardown_util(bottle_id, MacosGatewayTransport(),
|
||||
orchestrator_url=orchestrator_url, timeout=timeout)
|
||||
|
||||
|
||||
|
||||
@@ -184,10 +184,10 @@ class MacosGateway(Gateway):
|
||||
def provisioning_transport(self) -> GatewayTransport:
|
||||
"""The exec/cp transport git-gate provisioning stages per-bottle repos +
|
||||
deploy keys through (over the `container` CLI)."""
|
||||
# Local import: gateway_provision imports GATEWAY_NAME from this module,
|
||||
# so importing AppleGatewayTransport at module scope would cycle.
|
||||
from .gateway_provision import AppleGatewayTransport
|
||||
return AppleGatewayTransport(self.name)
|
||||
# Local import: gateway_transport imports GATEWAY_NAME from this module,
|
||||
# so importing MacosGatewayTransport at module scope would cycle.
|
||||
from .gateway_transport import MacosGatewayTransport
|
||||
return MacosGatewayTransport(self.name)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
+8
-9
@@ -1,10 +1,9 @@
|
||||
"""`GatewayTransport` for the Apple infra container (PRD 0070).
|
||||
"""The `GatewayTransport` for the Apple gateway container (PRD 0070).
|
||||
|
||||
The provisioning *logic* (per-bottle creds dirs, namespaced repo init) is
|
||||
backend-neutral and lives in `backend.docker.gateway_provision`; this is only
|
||||
the transport — how files and commands reach the running gateway. Docker uses
|
||||
`docker exec`/`docker cp` and Firecracker uses SSH; Apple uses the `container`
|
||||
CLI's equivalents against the infra container that hosts the gateway daemons.
|
||||
How the launcher stages files + runs commands in the running gateway container:
|
||||
the `container` CLI's exec/cp equivalents. The backend-neutral provisioning
|
||||
logic that drives it lives in `backend.docker.gateway_provision`; Docker uses
|
||||
`docker exec`/`docker cp` and Firecracker uses SSH.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -14,8 +13,8 @@ from . import util as container_mod
|
||||
from .gateway import GATEWAY_NAME
|
||||
|
||||
|
||||
class AppleGatewayTransport:
|
||||
"""`GatewayTransport` for the gateway daemons in the Apple infra container."""
|
||||
class MacosGatewayTransport:
|
||||
"""`GatewayTransport` for the gateway daemons in the Apple gateway container."""
|
||||
|
||||
def __init__(self, gateway: str = GATEWAY_NAME) -> None:
|
||||
self.gateway = gateway
|
||||
@@ -41,4 +40,4 @@ class AppleGatewayTransport:
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["AppleGatewayTransport", "GatewayProvisionError"]
|
||||
__all__ = ["MacosGatewayTransport"]
|
||||
Reference in New Issue
Block a user