fix(pipelock): write yaml to /etc/pipelock.yaml since image lacks /etc/pipelock

The pipelock image is distroless and does not contain /etc/pipelock/, so
docker cp to /etc/pipelock/pipelock.yaml fails with "Could not find the
file /etc/pipelock in container" — docker cp does not create missing
intermediate parent directories when targeting a stopped container, and
no shell is available in the image for a mkdir shim. Move the config
file to /etc/pipelock.yaml (directly under /etc, which always exists)
and update the --config argv to match. Also surface docker cp stderr in
the die message so future failures of this sort are debuggable.

Assisted-by: Claude Code
This commit is contained in:
2026-05-08 01:23:42 -04:00
parent 713424214e
commit f6c943fcad
+20 -29
View File
@@ -271,7 +271,7 @@ pipelock_write_yaml() {
# service name. The image runs `pipelock` as its CMD; we override
# with `run --config <path>` and the listen address.
# 2. `docker cp` the YAML config from the host mktemp dir into the
# container at /etc/pipelock/pipelock.yaml.
# container at /etc/pipelock.yaml.
#
# We use docker cp rather than `-v <host>:<container>` because Docker
# Desktop bind mounts have ownership / case-sensitivity quirks on
@@ -308,29 +308,15 @@ pipelock_start() {
die "pipelock yaml not found at ${host_yaml}; pipelock_write_yaml must run first"
fi
# Container layout: pipelock reads its config from /etc/pipelock/pipelock.yaml.
# We bring the container up with a `sleep` shim so we can `docker cp`
# the config in, then restart with the real command — this avoids a
# bind mount entirely and keeps the host file off the container's
# filesystem after the cp completes.
#
# Two-phase boot:
# phase 1: `docker run -d --entrypoint sh ... -c 'mkdir -p /etc/pipelock && sleep infinity'`
# so we can cp the YAML in before pipelock starts and
# tries to read it. We do NOT attach to internal_network
# here; we'll connect after the config is in place so the
# real pipelock process never sees a half-configured
# agent on the wire.
# phase 2: docker cp + docker restart with the real command via
# --entrypoint reset (handled below).
# Container layout: pipelock reads its config from /etc/pipelock.yaml.
# We `docker create` the sidecar, `docker cp` the YAML into the
# writable layer, then `docker start` it — no bind mount, no shell
# shim. The image is distroless (no `sh`), and `docker cp` to a
# stopped container does NOT create intermediate parent directories,
# so the YAML lives directly under /etc rather than in a /etc/pipelock
# subdirectory.
info "starting pipelock sidecar ${name} on network ${internal_network}"
# We cannot easily restart a container with a different command
# using `docker restart`. Instead, run the container in two stages:
# boot it with `sh -c 'mkdir + sleep'`, cp the file in, then start
# the real pipelock by docker exec'ing it as PID-N. A simpler
# approach: `docker create` + `docker cp` + `docker start`. Use that.
#
# Sidecar argv verification (PR #1 review). The pinned digest
# (CLAUDE_BOTTLE_PIPELOCK_IMAGE above) has:
# ENTRYPOINT ["/pipelock"]
@@ -348,18 +334,23 @@ pipelock_start() {
--name "$name" \
--network "$internal_network" \
"$CLAUDE_BOTTLE_PIPELOCK_IMAGE" \
run --config /etc/pipelock/pipelock.yaml --listen "0.0.0.0:${CLAUDE_BOTTLE_PIPELOCK_PORT}" \
run --config /etc/pipelock.yaml --listen "0.0.0.0:${CLAUDE_BOTTLE_PIPELOCK_PORT}" \
>/dev/null 2>&1; then
die "failed to create pipelock sidecar ${name}"
fi
# `docker cp` to a created-but-not-started container creates parent
# dirs as needed and works without the container running, since the
# cp is done against the writable layer directly.
if ! docker cp "$host_yaml" "${name}:/etc/pipelock/pipelock.yaml" >/dev/null 2>&1; then
# `docker cp` to a created-but-not-started container writes into the
# writable layer directly. The parent directory must already exist in
# the image — docker cp does NOT create missing intermediate dirs to
# a stopped container, contrary to a common assumption. The pipelock
# image is distroless (no `sh`), so we cannot prepopulate dirs with a
# shell shim either. We therefore put the config in /etc/pipelock.yaml
# (file directly under /etc) rather than /etc/pipelock/pipelock.yaml.
local cp_err
cp_err="$(docker cp "$host_yaml" "${name}:/etc/pipelock.yaml" 2>&1)" || {
docker rm -f "$name" >/dev/null 2>&1 || true
die "failed to copy pipelock yaml into ${name}"
fi
die "failed to copy pipelock yaml into ${name}: ${cp_err}"
}
# Attach to a per-agent user-defined bridge network for upstream
# egress. The internal network has no gateway by definition, so