Owner decision 2026-10-07: production's mongod becomes a one-member replica set (rs0), so that a backup can read every collection at one instant (a snapshot read session), which a standalone mongod can't. The unit runs mongod with --replSet rs0 and a 990 MB oplog; setup.sh converts it once, idempotently (PrivaPub stopped, mongod restarted, rs.initiate, the primary awaited, PrivaPub started); every connection string says directConnection=true, which works against the standalone too, so this code can deploy before the conversion. The CI's throwaway mongod and the pasture's are replica sets as well, so the snapshot path is what the tests exercise; MongoTopology tells which a mongod is, and TopologyTests holds the test mongod to it. The suite passes on it (906). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
90 lines
5.3 KiB
Bash
90 lines
5.3 KiB
Bash
# Shared by run.sh: the network, Caddy (its CA kept in a volume and copied to .ca/root.crt), Mongo and PrivaPub.
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; repo="$(cd "$here/../.." && pwd)"
|
|
port=${PASTURE_PORT:-6971} # PrivaPub straight from the workstation; PASTURE_PORT when 6971 is taken
|
|
net=privapub-pasture; publish="$here/.publish"; ca="$here/.ca"
|
|
# The network looks public (11.42.0.0/24 is never reached for real from inside the pasture), so peers that refuse private
|
|
# addresses with no switch to say otherwise (Pixelfed, Mbin, WordPress, Discourse, Fedify) federate too.
|
|
subnet=${PASTURE_SUBNET:-11.42.0.0/24}
|
|
|
|
site() { curl -k --resolve "$1:6443:127.0.0.1" "${@:2}"; }
|
|
|
|
wait_http() { # url tries
|
|
for _ in $(seq 1 "${2:-60}"); do curl -fsk -o /dev/null "$1" && return 0; sleep 2; done
|
|
echo "timed out waiting for $1" >&2; return 1
|
|
}
|
|
|
|
pasture_base_up() {
|
|
local dotnet=${DOTNET:-$(command -v dotnet || echo ~/.dotnet/dotnet)}
|
|
"$dotnet" publish "$repo/PrivaPub/PrivaPub.csproj" -c Release -r linux-x64 --self-contained true -o "$publish" -v quiet
|
|
cp "$here/appsettings.Pasture.json" "$publish/"
|
|
podman network exists $net || podman network create --subnet "$subnet" $net >/dev/null
|
|
podman volume exists pasture-caddy-data || podman volume create pasture-caddy-data >/dev/null
|
|
# (PrivaPub's uploads live in a volume: a container made again must not lose them)
|
|
podman volume exists pasture-privapub-media || podman volume create --label pasture=1 pasture-privapub-media >/dev/null
|
|
# a one-member replica set, as production's, so backups read at one instant
|
|
podman run -d --replace --name pasture-mongo --network $net --network-alias mongo docker.io/library/mongo:8 --quiet --replSet rs0 >/dev/null
|
|
for _ in $(seq 1 60); do podman exec pasture-mongo mongosh --quiet --eval 'db.hello()' >/dev/null 2>&1 && break; sleep 1; done
|
|
podman exec pasture-mongo mongosh --quiet --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongo:27017'}]})" >/dev/null
|
|
for _ in $(seq 1 60); do [ "$(podman exec pasture-mongo mongosh --quiet --eval 'db.hello().isWritablePrimary')" = "true" ] && break; sleep 1; done
|
|
caddy_up
|
|
local extra=()
|
|
for setting in ${PRIVAPUB_ENV:-}; do extra+=(-e "$setting"); done
|
|
podman run -d --replace --name pasture-privapub --network $net -p "127.0.0.1:$port:80" \
|
|
--sysctl net.ipv4.ip_unprivileged_port_start=0 -e ASPNETCORE_ENVIRONMENT=Pasture "${extra[@]}" -w /app -v "$publish:/app:Z,ro" \
|
|
-v pasture-privapub-media:/tmp/privapub-media \
|
|
mcr.microsoft.com/dotnet/runtime-deps:10.0 /app/PrivaPub >/dev/null
|
|
wait_http "http://127.0.0.1:$port/build.json"
|
|
rm -rf "$ca"; mkdir -p "$ca"
|
|
for _ in $(seq 1 60); do
|
|
podman cp pasture-caddy:/data/caddy/pki/authorities/local/root.crt "$ca/root.crt" 2>/dev/null && [ -s "$ca/root.crt" ] && break
|
|
curl -sk -o /dev/null --resolve privapub.test:6443:127.0.0.1 https://privapub.test:6443/build.json || true
|
|
sleep 1
|
|
done
|
|
[ -s "$ca/root.crt" ] || { echo "Caddy's root certificate could not be copied" >&2; return 1; }
|
|
chmod 644 "$ca/root.crt"
|
|
cat /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt 2>/dev/null > "$ca/bundle.pem" || true
|
|
cat "$ca/root.crt" >> "$ca/bundle.pem"
|
|
chmod 644 "$ca/bundle.pem"
|
|
}
|
|
|
|
# Caddy, with every site the Caddyfile serves as a name on the network, so a peer is added with its Caddyfile block alone.
|
|
# Its CA lives in the pasture-caddy-data volume, so recreating it (run.sh add, for a new site) keeps every peer's trust.
|
|
caddy_up() {
|
|
local aliases=""
|
|
for site in $(caddy_sites); do aliases="$aliases --network-alias $site"; done
|
|
# shellcheck disable=SC2086
|
|
podman run -d --replace --name pasture-caddy --network $net $aliases \
|
|
-p 127.0.0.1:6443:443 --sysctl net.ipv4.ip_unprivileged_port_start=0 -v "$here/Caddyfile:/etc/caddy/Caddyfile:Z,ro" \
|
|
-v pasture-caddy-data:/data docker.io/library/caddy:2 >/dev/null
|
|
}
|
|
|
|
# caddy_current: whether the running Caddy already answers every site of the Caddyfile on the network
|
|
caddy_current() {
|
|
local have
|
|
have=$(podman inspect pasture-caddy --format '{{range .NetworkSettings.Networks}}{{range .Aliases}}{{.}} {{end}}{{end}}' 2>/dev/null)
|
|
for site in $(caddy_sites); do case " $have " in *" $site "*) ;; *) return 1 ;; esac; done
|
|
}
|
|
|
|
# the sites the Caddyfile serves: "a.test {" and "a.test, b.test {" lines
|
|
caddy_sites() {
|
|
grep -E '^[a-z0-9.-]+\.test(, *[a-z0-9.-]+\.test)* *\{' "$here/Caddyfile" | sed 's/ *{.*//; s/,/ /g'
|
|
}
|
|
|
|
# pasture_down [--purge]: removes every pasture container with its anonymous volumes (images declare volumes, and
|
|
# without -v each run left them behind), the named volumes and the network. Caddy's CA is kept unless --purge, so a peer
|
|
# that was told to trust it still does after the next up.
|
|
pasture_down() {
|
|
podman ps -a --format '{{.Names}}' | grep '^pasture-' | xargs -r podman rm -f -v >/dev/null 2>&1 || true
|
|
local keep='^pasture-caddy-data$'
|
|
[ "${1:-}" = "--purge" ] && keep='^$'
|
|
podman volume ls --format '{{.Name}}' | grep '^pasture-' | grep -v "$keep" | xargs -r podman volume rm -f >/dev/null 2>&1 || true
|
|
podman network rm $net >/dev/null 2>&1 || true
|
|
rm -rf "$here/.state" "$ca"
|
|
}
|
|
|
|
# pasture_stats: memory and CPU of every pasture container, largest first
|
|
pasture_stats() {
|
|
podman stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' $(podman ps --format '{{.Names}}' | grep '^pasture-') 2>/dev/null \
|
|
| sort -t$'\t' -k2 -h -r
|
|
}
|