LibrePortal/scripts/migrate/migrate_discover.sh
librelad 32b2840d73 refactor(migrate)!: rewrite kernel — discover/preflight/apply with JSON progress
Phase 0 of the migration-system refresh. Replaces the 77-line
scripts/migrate/ with a properly-shaped kernel that Phase 1 (WebUI) and
Phase 3 (direct peer SSH) can both build on.

New module layout (6 files):
  migrate_progress.sh   — migrateEmit JSON-per-line helper; opt-in via
                          MIGRATE_JSON_PROGRESS=1, writes to fd 3 if open
                          (clean WebUI streaming channel) else stdout.
  migrate_discover.sh   — migrateDiscoverHosts / migrateDiscoverApps /
                          migrateDiscoverAppDetail (JSON {snapshots, latest_*}).
                          Old migrateDiscoverAppsForHost kept as back-compat.
  migrate_preflight.sh  — migratePreflight emits one JSON object with
                          snapshot{id,date}, destination{installed,running,
                          disk_free_kb}, collision{occurs,default_action,
                          pre_backup_default}, url_rewrite{default_action,
                          per_app_opt_out}, warnings[], errors[].
                          Exit 0 on usable preflight, 1 on hard error.
  migrate_url_rewrite.sh— Host-bound CFG_<APP>_* fields (URL/HOST/DOMAIN/
                          DOMAIN_PREFIX/HOSTNAME/PUBLIC_URL) get rewritten
                          from the destination's install-template after
                          restore — so a moved app stops claiming the
                          source's hostnames. Per-app opt-out via
                          CFG_<APP>_MIGRATE_URL_REWRITE=false. All other
                          fields (DB passwords, API keys, prefs) carry
                          over from the source unchanged.
  migrate_pre_backup.sh — migratePreBackupDestination takes a snapshot of
                          the destination's existing <app> (tagged
                          pre-migrate=<UTC timestamp>) before the wipe.
                          Default ON; opt-out with --no-pre-backup. Safety
                          net for the always-replace collision policy.
  migrate_apply.sh      — migrateApplyApp / migrateApplySystem. Parses
                          --no-pre-backup / --keep-urls / --json-progress
                          opts, runs preflight → pre-backup → restoreAppStart
                          (existing flow) → URL rewrite → re-deploy compose.
                          migrateApp / migrateSystem kept as shims so the
                          old CLI surface still works.

CLI dispatcher (cli_restore_commands.sh + cli_restore_header.sh):
  Existing 'restore migrate app/system/discover' calls all still work.
  New verbs:
    restore migrate list <host> [loc_idx]
    restore migrate preflight <host> <app> [loc_idx]   ← JSON, for the WebUI

Design choices baked in (per the spec):
  - Always-replace collision (no multi-install of an app), safety net is the
    on-by-default pre-migrate backup.
  - URL rewrite by host-bound suffix list, not per-field allowlist — works
    out-of-the-box for new apps without extra config.
  - migrateEmit fd-3 contract is what Phase 1's WebUI will stream; falls
    back to stdout in interactive CLI so dev/debug just works.
  - Transport-agnostic: nothing in this kernel knows whether the backup
    location is local/SSH/S3/Connect — engineSnapshotsJson + engineBackupApp
    do that, so Connect (the future blind-relay) plugs in as 'just another
    location kind' with zero kernel changes.

Smoke-tested: all 13 public functions register; JSON emit produces correct
escaping (quoted strings vs bare numerics) and respects MIGRATE_JSON_PROGRESS.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 17:22:54 +01:00

88 lines
2.8 KiB
Bash

#!/bin/bash
# Discovery helpers for cross-host migrate. All read-only — they query a
# backup location's snapshot index, never touch live state.
#
# Three views, increasing specificity:
# migrateDiscoverHosts — which hosts have snapshots in this location?
# migrateDiscoverApps — which apps does <host> have in this location?
# migrateDiscoverAppDetail — newest snapshot + count for one host/app.
# Resolve to the first enabled location when idx is unset. Returns "" if
# nothing is enabled — callers should treat that as "nowhere to discover from."
_migrateResolveLocation()
{
local idx="$1"
if [[ -z "$idx" ]]; then
idx=$(resticEnabledLocations | head -1)
fi
echo "$idx"
}
# List hostnames with at least one snapshot in the given location. One per line.
migrateDiscoverHosts()
{
local idx
idx=$(_migrateResolveLocation "$1")
[[ -z "$idx" ]] && return 1
engineSnapshotsJson "$idx" 2>/dev/null \
| grep -o '"hostname":"[^"]*"' \
| sort -u \
| cut -d'"' -f4
}
# List apps backed up by <host> in this location. One slug per line.
migrateDiscoverApps()
{
local idx
idx=$(_migrateResolveLocation "$2")
local host="$1"
[[ -z "$idx" || -z "$host" ]] && return 1
engineSnapshotsJson "$idx" "" "$host" 2>/dev/null \
| grep -o '"app=[^"]*"' \
| sort -u \
| sed 's/"app=\(.*\)"/\1/'
}
# JSON detail for one host+app: snapshot count + latest id/date.
# Output: {"host":"…","app":"…","snapshots":N,"latest_id":"…","latest_date":"…"}
# Missing snapshots → snapshots=0, latest_*=null.
migrateDiscoverAppDetail()
{
local host="$1"
local app="$2"
local idx
idx=$(_migrateResolveLocation "$3")
[[ -z "$idx" || -z "$host" || -z "$app" ]] && return 1
local json
json=$(engineSnapshotsJson "$idx" "$app" "$host" 2>/dev/null)
local count
count=$(printf '%s' "$json" | grep -oc '"short_id":"' || echo 0)
local latest_id="null"
local latest_date="null"
if (( count > 0 )); then
# Restic prints snapshots in creation order — last one is newest.
latest_id="\"$(printf '%s' "$json" \
| grep -o '"short_id":"[^"]*"' | tail -1 | cut -d'"' -f4)\""
latest_date="\"$(printf '%s' "$json" \
| grep -o '"time":"[^"]*"' | tail -1 | cut -d'"' -f4)\""
fi
printf '{"host":"%s","app":"%s","loc_idx":%s,"snapshots":%s,"latest_id":%s,"latest_date":%s}\n' \
"$host" "$app" "$idx" "$count" "$latest_id" "$latest_date"
}
# Back-compat shim — older callers (and the existing CLI) used this name with
# (idx, host) arg order. New code should call migrateDiscoverApps directly.
migrateDiscoverAppsForHost()
{
local idx="$1"
local host="$2"
migrateDiscoverApps "$host" "$idx"
}