539234f29e
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>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""DockerPipelockProxy — the Docker-specific implementation of the
|
|
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
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from ...log import die
|
|
from ...pipelock import PipelockProxy
|
|
|
|
|
|
# Pipelock image, pinned by digest. The digest is the multi-arch image
|
|
# index for ghcr.io/luckypipewrench/pipelock:2.3.0.
|
|
PIPELOCK_IMAGE = os.environ.get(
|
|
"CLAUDE_BOTTLE_PIPELOCK_IMAGE",
|
|
"ghcr.io/luckypipewrench/pipelock@sha256:3b1a39417b98406ddc5dc2d8fcb42865ddc0c68a43d355db55f0f8cb06bc6de9",
|
|
)
|
|
|
|
# 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 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"
|
|
|
|
|
|
def pipelock_container_name(slug: str) -> str:
|
|
return f"claude-bottle-pipelock-{slug}"
|
|
|
|
|
|
def pipelock_proxy_url(slug: str) -> str:
|
|
return f"http://{pipelock_container_name(slug)}:{PIPELOCK_PORT}"
|
|
|
|
|
|
def pipelock_tls_init(stage_dir: Path) -> tuple[Path, Path]:
|
|
"""Generate a fresh per-bottle CA via a one-shot pipelock container.
|
|
|
|
Runs `pipelock tls init` against a host-mounted scratch dir, leaving
|
|
`ca.pem` (public cert, mode 600) and `ca-key.pem` (private key, mode
|
|
600) under `<stage_dir>/pipelock-ca/`. Returns the two host paths.
|
|
|
|
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; 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(
|
|
["docker", "run", "--rm",
|
|
"-v", f"{work}:/h",
|
|
"-e", "PIPELOCK_HOME=/h",
|
|
PIPELOCK_IMAGE, "tls", "init"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
die(f"pipelock tls init failed: {result.stderr.strip()}")
|
|
cert = work / "ca.pem"
|
|
key = work / "ca-key.pem"
|
|
if not cert.is_file() or not key.is_file():
|
|
die(f"pipelock tls init did not produce ca files in {work}")
|
|
# Explicit perms in case a future pipelock release changes
|
|
# defaults. Pipelock runs as root in its distroless image and
|
|
# bind-mounts work with 0o600 (root reads everything); the key
|
|
# has no reason to be readable to anyone else on the host.
|
|
key.chmod(0o600)
|
|
cert.chmod(0o644)
|
|
return (cert, key)
|
|
|
|
|
|
class DockerPipelockProxy(PipelockProxy):
|
|
"""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
|
|
|