The `migration-smoke` job fails on the Gitea runner at `docker port ekklesia-database-tls-<pid> 5432` with `no public port '5432' published`; the later invalid-URL and `ECONNREFUSED 127.0.0.1:5432` failures all cascade from it. Gitea executes the job inside a container and drives sibling containers through the runner's Docker daemon. The harness held two host-only assumptions, both reproduced in a nested job container and fixed here. Bind-mount source paths are resolved by the daemon against the runner host's filesystem. The certificate directory is created in the job container's `/tmp`, which does not exist there, so the daemon silently creates an empty directory, the entrypoint's `cp` fails, the container exits, and `docker port` reports no published port for an exited container. The material is now transferred with `docker cp` into the created-but-not-yet-started container, which crosses the daemon boundary correctly in both topologies, and only the server leaf and its key are transferred — both CA private keys stay in the harness directory. A published port lands on the runner host's loopback, a different network namespace from the job container's; measured, a container the daemon published at `127.0.0.1:32907` is `ECONNREFUSED` at that address from inside the job container. Under a containerized job the harness now publishes nothing and joins both containers to one isolated user-defined network, reaching PostgreSQL by container DNS on 5432. Direct-host runs keep a random port pinned to loopback via Docker's `127.0.0.1::5432` form. `compose_job_network_job_container` exports the topology decision the integration script already made, so there is still exactly one detector. Every TLS assertion is preserved, and the certificate identity is generated from the host actually connected to — `localhost` directly, the container's DNS name otherwise. The mismatch and chain-only controls use the TLS container's address on the isolated network rather than a hardcoded one, and the certificate carries no IP SAN, so verify-full still fails with `ERR_TLS_CERT_ALTNAME_INVALID`. Correcting one measurement in passing: chain-only verification does not skip the identity check. For an IP host `pg` omits `servername` and hands `tls.connect` a bare socket with no host, and `net.Socket._host` is null for an IP literal, so Node checks the fixed fallback name `localhost` — never the address dialled. A certificate whose only SAN is `DNS:example.invalid`, reached chain-only over `127.0.0.1`, is rejected with `Host: localhost. is not in the cert's altnames`. The direct-host control therefore only ever passed because its SAN was `DNS:localhost`. The SAN set now always includes that fallback name so the control proves the same gap in both topologies, and the stale claim is corrected in the transport doc comment. No production behavior changes. Teardown also removes the container's anonymous volume, the isolated network, and the job container's attachment, after success and failure alike. Co-Authored-By: Claude Opus 5 <[email protected]>
124 lines
3.4 KiB
Bash
124 lines
3.4 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=false
|
|
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER=""
|
|
COMPOSE_JOB_NETWORK_NAME=""
|
|
COMPOSE_JOB_NETWORK_CONNECTED=false
|
|
|
|
compose_job_network_initialize() {
|
|
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=false
|
|
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER=""
|
|
COMPOSE_JOB_NETWORK_NAME=""
|
|
COMPOSE_JOB_NETWORK_CONNECTED=false
|
|
|
|
if [[ ! -f /.dockerenv ]]; then
|
|
return 0
|
|
fi
|
|
if [[ -z "${HOSTNAME:-}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=true
|
|
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER="$HOSTNAME"
|
|
}
|
|
|
|
# Emits the identifier of the container this job is executing in, or an empty string when the job
|
|
# runs directly on a host. This is the single topology signal: harnesses that provision their own
|
|
# containers instead of a Compose project (the TLS integration suite) consume it through the
|
|
# environment rather than re-deriving the topology, so there is exactly one detector.
|
|
compose_job_network_job_container() {
|
|
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" != "true" ]]; then
|
|
return 0
|
|
fi
|
|
printf '%s' "$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER"
|
|
}
|
|
|
|
compose_job_network_connect() {
|
|
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" != "true" ]]; then
|
|
return 1
|
|
fi
|
|
if (($# == 0)); then
|
|
return 1
|
|
fi
|
|
local service="$1"
|
|
shift
|
|
if [[ -z "$service" || $# -eq 0 ]]; then
|
|
return 1
|
|
fi
|
|
|
|
local service_container_output
|
|
if ! service_container_output="$("$@" ps -q "$service" 2>/dev/null)"; then
|
|
return 1
|
|
fi
|
|
|
|
local service_containers=()
|
|
while IFS= read -r container_id; do
|
|
if [[ -n "$container_id" ]]; then
|
|
service_containers+=("$container_id")
|
|
fi
|
|
done <<<"$service_container_output"
|
|
if ((${#service_containers[@]} != 1)); then
|
|
return 1
|
|
fi
|
|
|
|
local service_network_output
|
|
if ! service_network_output="$(
|
|
docker inspect \
|
|
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
|
|
"${service_containers[0]}" 2>/dev/null
|
|
)"; then
|
|
return 1
|
|
fi
|
|
|
|
local service_networks=()
|
|
while IFS= read -r network_name; do
|
|
if [[ -n "$network_name" ]]; then
|
|
service_networks+=("$network_name")
|
|
fi
|
|
done <<<"$service_network_output"
|
|
if ((${#service_networks[@]} != 1)); then
|
|
return 1
|
|
fi
|
|
COMPOSE_JOB_NETWORK_NAME="${service_networks[0]}"
|
|
|
|
local current_network_output
|
|
if ! current_network_output="$(
|
|
docker inspect \
|
|
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
|
|
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" 2>/dev/null
|
|
)"; then
|
|
return 1
|
|
fi
|
|
|
|
while IFS= read -r network_name; do
|
|
if [[ "$network_name" == "$COMPOSE_JOB_NETWORK_NAME" ]]; then
|
|
COMPOSE_JOB_NETWORK_CONNECTED=true
|
|
return 0
|
|
fi
|
|
done <<<"$current_network_output"
|
|
|
|
COMPOSE_JOB_NETWORK_CONNECTED=true
|
|
if ! docker network connect \
|
|
"$COMPOSE_JOB_NETWORK_NAME" \
|
|
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1; then
|
|
docker network disconnect \
|
|
"$COMPOSE_JOB_NETWORK_NAME" \
|
|
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1 || true
|
|
COMPOSE_JOB_NETWORK_CONNECTED=false
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
compose_job_network_disconnect() {
|
|
if [[ "$COMPOSE_JOB_NETWORK_CONNECTED" != "true" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if ! docker network disconnect \
|
|
"$COMPOSE_JOB_NETWORK_NAME" \
|
|
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
COMPOSE_JOB_NETWORK_CONNECTED=false
|
|
}
|