Exhaustive audit (workflow: 19 finders + adversarial per-file verify; 85 raw -> 66 unique -> 39 confirmed) found 36 direct writes into the container-owned tree that bypass runFileOp/runFileWrite/runCfgOp (manager => EACCES in rootless) plus 3 $?-masking sites. Fixes by area: - apps: grafana + prometheus install hooks (sudo chmod -> runFileOp chmod); gluetun provider etag (tee -> runFileWrite). - webui generators: task-create (10 sites: mkdir/chown/tee/jq|tee/sed|tee -> runFileOp/runFileWrite); app-icons (mkdir/cp/mv); config icon cp; system metrics + update throttle stamps (runAsManager touch -> runFileOp touch); setup-lock rm; updater history seed + cp. - task health checker: 4 log writes (tee -a -> runFileWrite -a) + 3 find -delete (-> runFileOp find). - config reconcile: backup cp -> runCfgOp; live cp -> runFileWrite < tmp for container-owned configs (the container user can't read a manager 0600 tmp). - peer pull: tar extract into the container tree -> runFileOp tar. - masking: ip_find_available + folder_group(x2) — split 'local VAR=$(cmd)' so $? reaches the following [[ $? ]] check. 15 files, all pass bash -n; fixed idioms confirmed gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
131 lines
5.4 KiB
Bash
131 lines
5.4 KiB
Bash
#!/bin/bash
|
|
|
|
# Pull an app from a direct-ssh-direct peer onto this host. End-to-end:
|
|
#
|
|
# 1. Resolve peer + verify reachable (peerPing).
|
|
# 2. Take the existing-app safety backup (migratePreBackupDestination) so
|
|
# a bad pull is rollback-able via the normal restore flow.
|
|
# 3. Stop + wipe the destination's app folder (same as restoreAppStart).
|
|
# 4. Stream `peer-shell stream-app <slug>` over SSH and untar into
|
|
# containers_dir/.
|
|
# 5. Reuse the install-time tag pipeline to repoint host-bound values:
|
|
# - dockerComposeUpdateAndStartApp <app> install re-emits compose
|
|
# - migrateApplyUrlRewrite rewrites URLs
|
|
# 6. Start the container, run app-specific post-restore hooks.
|
|
#
|
|
# That gives us the same idempotent install path the restic-mediated
|
|
# migrateApplyApp uses, just with tar-over-SSH as the data source instead
|
|
# of a restic snapshot.
|
|
|
|
# peerPullApp <peer-name> <app-slug> [--no-pre-backup] [--keep-urls]
|
|
peerPullApp()
|
|
{
|
|
local peer_name="$1"; shift
|
|
local app="$1"; shift
|
|
|
|
# Reuse migrate_apply's opt parser to keep flag semantics identical.
|
|
_migrateParseOpts "$@"
|
|
|
|
if [[ -z "$peer_name" || -z "$app" ]]; then
|
|
isError "peerPullApp: peer_name and app required"
|
|
return 1
|
|
fi
|
|
|
|
local row; row=$(peerGet "$peer_name")
|
|
if [[ -z "$row" || "$row" == "null" ]]; then
|
|
isError "No peer named '$peer_name'"
|
|
return 1
|
|
fi
|
|
local kind
|
|
kind=$(printf '%s' "$row" | grep -o '"kind":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
if [[ "$kind" != "direct-ssh-direct" ]]; then
|
|
isError "Peer '$peer_name' is kind=$kind — peerPullApp needs direct-ssh-direct"
|
|
return 1
|
|
fi
|
|
|
|
local started_at; started_at=$(date +%s)
|
|
isHeader "Pull $app from peer=$peer_name → this host"
|
|
migrateEmit phase=start status=running app="$app" peer="$peer_name" transport=direct-ssh
|
|
|
|
# ---- 1. Reachability --------------------------------------------------
|
|
migrateEmit phase=ping status=running peer="$peer_name"
|
|
local ping_status; ping_status=$(peerPing "$peer_name")
|
|
if [[ "$ping_status" != "ok" ]]; then
|
|
isError "Peer '$peer_name' unreachable: $ping_status"
|
|
migrateEmit phase=ping status=failed detail="$ping_status"
|
|
return 1
|
|
fi
|
|
migrateEmit phase=ping status=complete
|
|
|
|
# ---- 2. Pre-backup of destination (default ON) ------------------------
|
|
if (( MIGRATE_OPT_PRE_BACKUP )); then
|
|
migratePreBackupDestination "$app"
|
|
else
|
|
migrateEmit phase=pre-backup status=skipped reason=user-opt-out app="$app"
|
|
fi
|
|
|
|
# Per-app pre-migrate hook (optional) — declared in the app's tools.sh.
|
|
migrateRunHook "$app" pre "$peer_name" "direct-ssh"
|
|
|
|
# ---- 3. Stop + wipe ----------------------------------------------------
|
|
migrateEmit phase=stop status=running app="$app"
|
|
if declare -f dockerComposeDown >/dev/null 2>&1 && [[ -d "$containers_dir$app" ]]; then
|
|
dockerComposeDown "$app" >/dev/null 2>&1 || true
|
|
fi
|
|
if [[ -d "$containers_dir$app" ]]; then
|
|
runFileOp rm -rf "${containers_dir:?}$app"
|
|
fi
|
|
migrateEmit phase=stop status=complete app="$app"
|
|
|
|
# ---- 4. Stream tar over SSH and untar ---------------------------------
|
|
migrateEmit phase=transfer status=running app="$app"
|
|
# The pipe gives us streaming throughput without staging the whole tarball
|
|
# to disk. set -o pipefail catches ssh failures so we don't run the rest
|
|
# of the flow on a partial extract.
|
|
(
|
|
set -o pipefail
|
|
peerExec "$peer_name" "stream-app $app" | runFileOp tar -C "$containers_dir" -xf -
|
|
)
|
|
local transfer_rc=$?
|
|
if (( transfer_rc != 0 )); then
|
|
isError "Transfer of $app from $peer_name failed (rc=$transfer_rc)"
|
|
migrateEmit phase=transfer status=failed rc="$transfer_rc"
|
|
return 1
|
|
fi
|
|
if [[ ! -d "$containers_dir$app" ]]; then
|
|
isError "Transfer reported success but $containers_dir$app missing"
|
|
migrateEmit phase=transfer status=failed reason=missing-output
|
|
return 1
|
|
fi
|
|
runFileOp chown -R "${docker_install_user:-$(whoami)}":"${docker_install_user:-$(whoami)}" "$containers_dir$app" 2>/dev/null || true
|
|
migrateEmit phase=transfer status=complete app="$app"
|
|
|
|
# ---- 5. URL rewrite (default ON) + re-deploy compose -------------------
|
|
if ! (( MIGRATE_OPT_KEEP_URLS )); then
|
|
migrateApplyUrlRewrite "$app"
|
|
else
|
|
migrateEmit phase=url-rewrite status=skipped reason=user-opt-out app="$app"
|
|
fi
|
|
if declare -f dockerComposeUpdateAndStartApp >/dev/null 2>&1; then
|
|
dockerComposeUpdateAndStartApp "$app" install >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# ---- 6. Start + post-restore hooks ------------------------------------
|
|
migrateEmit phase=start-app status=running app="$app"
|
|
if declare -f dockerComposeUp >/dev/null 2>&1; then
|
|
dockerComposeUp "$app" >/dev/null 2>&1 || true
|
|
fi
|
|
if declare -f restoreAppRunHook >/dev/null 2>&1; then
|
|
restoreAppRunHook "$app" post || true
|
|
fi
|
|
migrateEmit phase=start-app status=complete app="$app"
|
|
|
|
# Per-app post-migrate hook (optional) — last thing before declaring done.
|
|
migrateRunHook "$app" post "$peer_name" "direct-ssh"
|
|
|
|
local finished_at; finished_at=$(date +%s)
|
|
local duration=$((finished_at - started_at))
|
|
isSuccessful "Pulled $app from $peer_name in ${duration}s"
|
|
migrateEmit phase=done status=complete app="$app" duration_seconds="$duration"
|
|
}
|