Ctrl K
ComunidadGuías…

app/services/backup_runner/backup.sh

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# shellcheck shell=bash
# Executed inside the docker:cli sidecar by BackupRunner. The container runs
# Alpine `ash` (not bash); `shell=bash` is the closest dialect Shellcheck
# supports that allows `set -o pipefail`.
# Receives positional args via `sh -c '<this script>' _ <token> <filename>
# <date> <postgres-container> <influxdb-container>`. Positional args are
# passed by argv (not interpolated) so values with shell metacharacters are
# safe. The InfluxDB inner pipeline matches what HELIOS ran before (`influx
# backup` + tar+gzip), just orchestrated from outside.
#
# This script no longer talks to the backup destination directly: it writes
# the final tar (or an error file) into /output (the staging dir) and exits.
# The HELIOS Rails process picks the artifact up afterwards and handles
# any remote upload via aws-sdk-s3.

set -eu
set -o pipefail

TOKEN="$1"
BACKUP_FILENAME="$2"
BACKUP_DATE="$3"
POSTGRES_CONTAINER="$4"
INFLUXDB_CONTAINER="$5"

OUTPUT_DIR="/output"
RUNTIME_DIR="/runtime"
# Intermediate dumps + config copy live on the sidecar's own writable layer
# (a plain path under `/`, not a bind mount), never on /output or /runtime.
# Three reasons: (1) on a remote destination (NAS/SMB/S3 staging) writing
# multi-GB dumps there is wasteful — the final tar is the only artifact that
# has to land at the destination; (2) macOS SMB creates `.smbdelete*`
# tombstones when files are unlinked in-place, which causes `rmdir` on the
# work dir to fail with "Directory not empty" on the next run; (3) the
# bundling step symlinks the InfluxDB staging dir into WORK_DIR (see below)
# and `tar -h` dereferences it — but the /runtime bind mount is often backed
# by a host filesystem that rejects symlink() with EPERM (notably Unraid's
# `/mnt/user` FUSE user shares, issue #305). The container layer always
# supports symlinks and is discarded with `--rm`, so it sidesteps all three.
WORK_DIR="/work"
PG_FILE="$WORK_DIR/solectrus-postgresql-backup-$BACKUP_DATE.sql.gz"
CONFIG_DEST="$WORK_DIR/helios/config.yaml"
# Shared bind mount: InfluxDB writes its backup directly here, and we
# tar-stream it straight into the final archive without a docker-exec
# pipe or a second gzip pass. The same path exists in both containers.
INFLUX_STAGING="/influx-backup-staging"
INFLUX_NAME="solectrus-influxdb-backup-$BACKUP_DATE"
INFLUX_DIR="$INFLUX_STAGING/$INFLUX_NAME"
PART_PATH="$OUTPUT_DIR/$BACKUP_FILENAME.part"
FINAL_PATH="$OUTPUT_DIR/$BACKUP_FILENAME"
# Error + phase markers share the local /runtime dir, never /output. If the
# destination itself is the problem (full disk, read-only NFS, vanished
# mount, root_squash permission-denied), writing the failure note to
# /output would fail too — HELIOS would then only see the generic
# "process stopped" fallback. /runtime is HELIOS-local and always writable.
ERROR_PATH="$RUNTIME_DIR/error.txt"
PHASE_PATH="$RUNTIME_DIR/backup-phase.txt"
# Diagnostic logs for the inner dump commands. Kept in /runtime so a successful
# run never tars them into the final archive; the previous run's logs are
# wiped on startup below so stale output doesn't mislead future debugging.
PG_LOG="$RUNTIME_DIR/postgresql-dump.log"
INFLUX_LOG="$RUNTIME_DIR/influxdb-backup.log"

# Atomic write so BackupRunner.current_phase never reads a half-written name.
set_phase() {
  printf '%s\n' "$1" > "$PHASE_PATH.tmp" && mv "$PHASE_PATH.tmp" "$PHASE_PATH"
}

fail() {
  echo "$1" > "$ERROR_PATH"
  rm -f "$PHASE_PATH"
  rm -rf "$WORK_DIR"
  rm -rf "$INFLUX_DIR"
  rm -f "$PART_PATH"
  exit 1
}

rm -rf "$WORK_DIR" || fail "Failed to clean work directory: $WORK_DIR"
rm -f "$PART_PATH" "$ERROR_PATH" "$PHASE_PATH" "$PG_LOG" "$INFLUX_LOG"
mkdir -p "$WORK_DIR/helios"

set_phase dumping_postgres
# pg_dump's stderr is redirected to a log so the actual cause (e.g. "database
# does not exist") surfaces in error.txt instead of just the generic prefix.
if ! docker exec "$POSTGRES_CONTAINER" \
       pg_dump -U postgres --clean --if-exists --dbname=solectrus_production \
     2> "$PG_LOG" \
     | gzip > "$PG_FILE"; then
  fail "PostgreSQL dump failed: $(tail -n 20 "$PG_LOG" | tr '\n' ' ')"
fi

[ -s "$PG_FILE" ] || fail "PostgreSQL dump produced empty output"

set_phase dumping_influx
# Clear any leftover staging from an interrupted previous run, then let
# `influx backup` write straight into the shared mount. No stdio pipe,
# no /tmp page-cache pressure inside the InfluxDB container.
rm -rf "$INFLUX_DIR" || fail "Failed to clean InfluxDB staging dir: $INFLUX_DIR"
if ! docker exec "$INFLUXDB_CONTAINER" \
       influx backup "$INFLUX_DIR" -t "$TOKEN" \
     2> "$INFLUX_LOG"; then
  fail "InfluxDB backup failed: $(tail -n 20 "$INFLUX_LOG" | tr '\n' ' ')"
fi

if [ ! -d "$INFLUX_DIR" ] || [ -z "$(ls -A "$INFLUX_DIR" 2>/dev/null)" ]; then
  fail "InfluxDB backup produced no output"
fi

set_phase bundling
cp /config.yaml "$CONFIG_DEST" || fail "Failed to copy config.yaml"

# The InfluxDB backup lives on the staging bind-mount, separate from the
# rest of the work dir (the sidecar's own layer), and BusyBox tar applies
# only the last `-C` globally and lacks `-r` (append) and `--transform`.
# Symlink the staging dir into the work dir and let `tar -h` dereference
# it — the files land under solectrus-influxdb-backup-DATE/ in the archive
# without ever copying multi-GB out of the staging mount.
ln -s "$INFLUX_DIR" "$WORK_DIR/$INFLUX_NAME" \
  || fail "Failed to link InfluxDB backup into work directory"

# Outer tar is uncompressed: PG dump is already gzipped, and `influx
# backup` ships its own *.tar.gz / *.bolt.gz / *.sqlite.gz — a second
# gzip layer would just burn CPU on a (often Pi-class) host.
tar -cf "$PART_PATH" -C "$WORK_DIR" -h . \
  || fail "Failed to bundle backup archive"

mv "$PART_PATH" "$FINAL_PATH" || fail "Failed to publish backup archive: $FINAL_PATH"
# PHASE_PATH is deliberately NOT removed here. There is a small window
# between this line and the actual container exit (the rm -rf calls
# below) during which /backups can poll while the container is still
# running. Without the marker, BackupProgress' current_index falls back
# to script_phases.first and the UI jumps from :bundling back to
# :dumping_postgres for ~1-3 s. The next run wipes PHASE_PATH on entry
# (see the top-of-script cleanup), so leaving a zombie marker is safe.
rm -rf "$WORK_DIR" || fail "Failed to clean work directory after backup: $WORK_DIR"
rm -rf "$INFLUX_DIR" || fail "Failed to clean InfluxDB staging dir after backup: $INFLUX_DIR"