refactor(sidecars): drop vestigial start/stop methods (PRD 0024 chunk 3)
Compose-up has owned per-container lifecycle since PRD 0018 ch3;
the .start() / .stop() methods on DockerPipelockProxy /
DockerEgress / DockerGitGate / DockerSupervise (and their
abstractmethod declarations in the four base ABCs) were already
documented as vestigial. With the bundle path in flight
(PRD 0024 ch2), they are truly dead — collapse to nothing.
Changes:
- Removed start/stop methods from the four DockerSidecar
classes. Plan dataclasses, image/path constants,
container-name helpers, and the .prepare() methods all stay
(the renderer + apply path still need them).
- Removed the matching @abstractmethod declarations in the
base ABCs so concrete subclasses don't have to stub them.
- launch.launch() and prepare.resolve_plan() no longer take
proxy/git_gate/egress/supervise instance parameters. backend.py
loses the four instance attributes it threaded through.
prepare.resolve_plan() instantiates the four classes itself
to call their .prepare() methods.
- Deleted four integration tests that only exercised the
removed lifecycle: test_pipelock_sidecar_smoke,
test_supervise_sidecar, test_git_gate_sidecar,
test_git_gate_mirror.
- Dropped the .stop-idempotency case in test_orphan_cleanup;
the network-cleanup cases stay (those test real production
code).
- Marked test_pipelock_apply @skip pending chunk 4 — its
bringup helper used .start; chunk 4 rewrites it with direct
`docker run`.
Dockerfile deletion deferred to chunk 5 (when the bundle flag
default flips) — the legacy compose path still needs
Dockerfile.{egress,git-gate,supervise} until then.
Net: 708 lines removed, 80 added.
533 unit tests + 27 integration tests passing (5 skipped: the
chunk-4-pending case + existing GITEA_ACTIONS guards).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
"""DockerPipelockProxy — the Docker-specific implementation of the
|
||||
sidecar's start/stop lifecycle. Inherits the platform-agnostic
|
||||
YAML-config generation from PipelockProxy."""
|
||||
sidecar's `.prepare()` step + in-container CA path constants.
|
||||
Inherits the platform-agnostic YAML-config generation from
|
||||
PipelockProxy.
|
||||
|
||||
The per-container `.start()` / `.stop()` lifecycle was deleted in
|
||||
PRD 0024 chunk 3 — compose-up owns the container lifecycle (PRD
|
||||
0018) and the bundle path (PRD 0024) collapses pipelock + egress
|
||||
+ git-gate + supervise into one container. What remains here is
|
||||
the prepare-time YAML rendering + the CA path constants the
|
||||
compose renderer reads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,8 +16,8 @@ import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from ...log import die, info, warn
|
||||
from ...pipelock import PipelockProxy, PipelockProxyPlan
|
||||
from ...log import die
|
||||
from ...pipelock import PipelockProxy
|
||||
|
||||
|
||||
# Pipelock image, pinned by digest. The digest is the multi-arch image
|
||||
@@ -22,9 +30,9 @@ PIPELOCK_IMAGE = os.environ.get(
|
||||
# Listening port for pipelock's forward proxy.
|
||||
PIPELOCK_PORT = os.environ.get("CLAUDE_BOTTLE_PIPELOCK_PORT", "8888")
|
||||
|
||||
# In-container paths where the per-bottle CA cert + key land after
|
||||
# `docker cp` in `DockerPipelockProxy.start`. Pipelock's rendered
|
||||
# YAML references these paths under `tls_interception`.
|
||||
# In-container paths where the per-bottle CA cert + key land via
|
||||
# the compose renderer's bind-mounts. Pipelock's rendered YAML
|
||||
# references these paths under `tls_interception`.
|
||||
PIPELOCK_CA_CERT_IN_CONTAINER = "/etc/pipelock-ca.pem"
|
||||
PIPELOCK_CA_KEY_IN_CONTAINER = "/etc/pipelock-ca-key.pem"
|
||||
|
||||
@@ -46,10 +54,10 @@ def pipelock_tls_init(stage_dir: Path) -> tuple[Path, Path]:
|
||||
|
||||
The image is pinned (same digest the running sidecar uses) so the
|
||||
generated CA matches what the sidecar expects. Output is owned by
|
||||
whatever UID the one-shot ran as; `DockerPipelockProxy.start`
|
||||
`docker cp`s the files into the sidecar's filesystem layer, so
|
||||
runtime ownership inside the sidecar (root in pipelock's
|
||||
distroless image) is independent."""
|
||||
whatever UID the one-shot ran as; the compose renderer's
|
||||
bind-mounts pin the files in place at runtime, so ownership
|
||||
inside the running sidecar (root in pipelock's distroless image)
|
||||
is independent."""
|
||||
work = stage_dir / "pipelock-ca"
|
||||
work.mkdir(exist_ok=True)
|
||||
result = subprocess.run(
|
||||
@@ -77,117 +85,10 @@ def pipelock_tls_init(stage_dir: Path) -> tuple[Path, Path]:
|
||||
|
||||
|
||||
class DockerPipelockProxy(PipelockProxy):
|
||||
"""Brings the pipelock sidecar up and down via Docker."""
|
||||
"""Docker-flavored PipelockProxy: inherits `.prepare()` from the
|
||||
base, exposes the in-container CA paths the renderer reads.
|
||||
Container lifecycle is owned by compose."""
|
||||
|
||||
CA_CERT_IN_CONTAINER = PIPELOCK_CA_CERT_IN_CONTAINER
|
||||
CA_KEY_IN_CONTAINER = PIPELOCK_CA_KEY_IN_CONTAINER
|
||||
|
||||
def start(self, plan: PipelockProxyPlan) -> str:
|
||||
"""Boot the pipelock sidecar:
|
||||
1. `docker create` on the internal network with the canonical
|
||||
name and argv `run --config /etc/pipelock.yaml --listen
|
||||
0.0.0.0:<port>`.
|
||||
2. `docker cp` the YAML config to /etc/pipelock.yaml.
|
||||
3. `docker cp` the CA cert + key to /etc/pipelock-ca.pem
|
||||
and /etc/pipelock-ca-key.pem (pipelock runs as root in
|
||||
its distroless image, so no chown is needed).
|
||||
4. Attach to the per-agent egress network.
|
||||
5. `docker start`.
|
||||
Returns the container name (the proxy_target passed to .stop)."""
|
||||
name = pipelock_container_name(plan.slug)
|
||||
if not plan.yaml_path.is_file():
|
||||
die(
|
||||
f"pipelock yaml not found at {plan.yaml_path}; "
|
||||
f"PipelockProxy.prepare must run first"
|
||||
)
|
||||
if not plan.ca_cert_host_path.is_file() or not plan.ca_key_host_path.is_file():
|
||||
die(
|
||||
f"pipelock CA missing at {plan.ca_cert_host_path} / "
|
||||
f"{plan.ca_key_host_path}; pipelock_tls_init must run first"
|
||||
)
|
||||
|
||||
info(f"starting pipelock sidecar {name} on network {plan.internal_network}")
|
||||
|
||||
create_args = [
|
||||
"docker", "create",
|
||||
"--name", name,
|
||||
"--network", plan.internal_network,
|
||||
PIPELOCK_IMAGE,
|
||||
"run", "--config", "/etc/pipelock.yaml",
|
||||
"--listen", f"0.0.0.0:{PIPELOCK_PORT}",
|
||||
]
|
||||
create_result = subprocess.run(
|
||||
create_args, capture_output=True, text=True, check=False,
|
||||
)
|
||||
if create_result.returncode != 0:
|
||||
die(
|
||||
f"failed to create pipelock sidecar {name}: "
|
||||
f"{create_result.stderr.strip()}"
|
||||
)
|
||||
|
||||
for src, dst, label in (
|
||||
(plan.yaml_path, "/etc/pipelock.yaml", "yaml"),
|
||||
(plan.ca_cert_host_path, PIPELOCK_CA_CERT_IN_CONTAINER, "ca cert"),
|
||||
(plan.ca_key_host_path, PIPELOCK_CA_KEY_IN_CONTAINER, "ca key"),
|
||||
):
|
||||
cp_result = subprocess.run(
|
||||
["docker", "cp", str(src), f"{name}:{dst}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if cp_result.returncode != 0:
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", name],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
|
||||
)
|
||||
die(f"failed to copy pipelock {label} into {name}: {cp_result.stderr.strip()}")
|
||||
|
||||
connect_result = subprocess.run(
|
||||
["docker", "network", "connect", plan.egress_network, name],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if connect_result.returncode != 0:
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", name],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
|
||||
)
|
||||
die(
|
||||
f"failed to attach pipelock sidecar {name} to egress network "
|
||||
f"{plan.egress_network}: {connect_result.stderr.strip()}"
|
||||
)
|
||||
|
||||
start_result = subprocess.run(
|
||||
["docker", "start", name], capture_output=True, text=True, check=False,
|
||||
)
|
||||
if start_result.returncode != 0:
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", name],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
|
||||
)
|
||||
die(
|
||||
f"failed to start pipelock sidecar {name}: "
|
||||
f"{start_result.stderr.strip()}"
|
||||
)
|
||||
|
||||
return name
|
||||
|
||||
def stop(self, proxy_target: str) -> None:
|
||||
"""Idempotent: missing container is success. `proxy_target` is
|
||||
the container name returned by .start."""
|
||||
if subprocess.run(
|
||||
["docker", "inspect", proxy_target],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
).returncode == 0:
|
||||
if subprocess.run(
|
||||
["docker", "rm", "-f", proxy_target],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
).returncode != 0:
|
||||
warn(
|
||||
f"failed to remove pipelock sidecar {proxy_target}; "
|
||||
f"clean up with 'docker rm -f {proxy_target}'"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user