backend: harden rootless app lifecycle orchestration

This commit is contained in:
archipelago
2026-06-11 00:24:32 -04:00
parent 09ec64932f
commit c393b96da3
56 changed files with 7543 additions and 1994 deletions
+44 -10
View File
@@ -98,6 +98,11 @@ alloc_port() {
# Run as archipelago user — podman sees rootless containers directly.
# Use sudo only for chown/mkdir operations.
PODMAN="podman"
PODMAN_IMAGE_CHECK_TIMEOUT="${PODMAN_IMAGE_CHECK_TIMEOUT:-10}"
podman_bounded() {
timeout --kill-after=2s "${PODMAN_IMAGE_CHECK_TIMEOUT}s" "$PODMAN" "$@"
}
# ── Pre-flight ───────────────────────────────────────────────────────
header "╔══════════════════════════════════════════════════╗"
@@ -152,7 +157,7 @@ container_image_id() {
}
spec_image_id() {
$PODMAN image inspect "$SPEC_IMAGE" --format '{{.Id}}' 2>/dev/null
podman_bounded image inspect "$SPEC_IMAGE" --format '{{.Id}}' 2>/dev/null
}
container_network() {
@@ -218,6 +223,39 @@ prepare_bind_source() {
esac
}
ensure_catatonit() {
command -v catatonit >/dev/null 2>&1 && return 0
$CHECK_ONLY && { info "catatonit missing (would install)"; return 0; }
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update >/dev/null 2>&1 || true
sudo apt-get install -y catatonit >/dev/null 2>&1 || true
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y catatonit >/dev/null 2>&1 || true
elif command -v apk >/dev/null 2>&1; then
sudo apk add catatonit >/dev/null 2>&1 || true
fi
command -v catatonit >/dev/null 2>&1 || { fail "catatonit missing; Portainer compose builds may fail"; return 1; }
}
ensure_portainer_host_paths() {
ensure_catatonit
if $CHECK_ONLY; then
[ -d /var/lib/archipelago/portainer/compose ] || info "Portainer compose dir missing (would create)"
[ -e /data ] || info "/data host path missing (would link to /var/lib/archipelago/portainer)"
return 0
fi
sudo mkdir -p /var/lib/archipelago/portainer/compose 2>/dev/null || true
sudo chown -R 1000:1000 /var/lib/archipelago/portainer 2>/dev/null || true
if [ ! -e /data ]; then
sudo ln -s /var/lib/archipelago/portainer /data 2>/dev/null || true
elif [ -d /data ] && [ ! -L /data ] && [ ! -e /data/compose ]; then
sudo ln -s /var/lib/archipelago/portainer/compose /data/compose 2>/dev/null || true
fi
}
container_has_mount() {
local name="$1" source="$2" target="$3"
$PODMAN inspect "$name" --format '{{range .Mounts}}{{println .Source "|" .Destination}}{{end}}' 2>/dev/null \
@@ -250,13 +288,7 @@ container_env_val() {
URL_ENV_SUFFIXES="_URL _HOST _ENDPOINT"
image_exists() {
# Note: `grep -q` closes stdin after first match → SIGPIPE (exit 141) on podman.
# With `set -o pipefail` active in the parent script, that propagates as failure
# and spuriously skips local-image containers. Use a full scan + explicit match
# check to keep the exit code stable regardless of pipefail.
local images
images=$($PODMAN images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null)
echo "$images" | grep -qF "$1"
podman_bounded image exists "$1" >/dev/null 2>&1
}
resolve_spec_image() {
@@ -280,7 +312,7 @@ resolve_spec_image() {
fi
done
repo=$($PODMAN images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
repo=$(podman_bounded images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
| grep -E "/${image_name}:${image_tag}$" \
| head -1 || true)
if [ -n "$repo" ]; then
@@ -377,6 +409,8 @@ reconcile() {
return
fi
[ "$name" = "portainer" ] && ensure_portainer_host_paths
# Filter by tier
[ -n "$FILTER_TIER" ] && [ "$SPEC_TIER" != "$FILTER_TIER" ] && return
@@ -701,7 +735,7 @@ BTCEOF
# bitcoin_rw.conf, so clean both files.
for conf in "$BITCOIN_CONF" "/var/lib/archipelago/bitcoin/bitcoin_rw.conf"; do
if [ -f "$conf" ]; then
sudo sed -i '/^server=/d; /^txindex=/d; /^rpcbind=/d; /^rpcallowip=/d; /^rpcport=/d; /^listen=/d; /^bind=/d; /^dbcache=/d' "$conf" 2>/dev/null
sudo sed -i '/^server=/d; /^txindex=/d; /^rpcbind=/d; /^rpcallowip=/d; /^rpcport=/d; /^listen=/d; /^bind=/d; /^dbcache=/d; /^rpcthreads=/d; /^rpcworkqueue=/d' "$conf" 2>/dev/null
fi
done
sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin 2>/dev/null