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>
103 lines
4.1 KiB
Bash
103 lines
4.1 KiB
Bash
#!/bin/bash
|
|
|
|
# Cross-host migrate inherits the source's CFG_<APP>_* values via the restored
|
|
# config file. Most of them (DB passwords, API keys, user preferences) are
|
|
# data and must survive the move. A small set are host-bound — they describe
|
|
# where the app lives — and would 404 or break TLS if we kept them pointing
|
|
# at the source. This module identifies and rewrites those.
|
|
#
|
|
# Host-bound fields are detected by suffix (URL, HOST, DOMAIN, DOMAIN_PREFIX),
|
|
# and the rewrite source-of-truth is the destination's install-template
|
|
# config (containers/<app>/<app>.config under the install tree) — that's what
|
|
# a fresh install on this box would produce.
|
|
#
|
|
# Per-app opt-out: set CFG_<APP>_MIGRATE_URL_REWRITE="false" in the app's
|
|
# install-template config to skip the rewrite (e.g. for apps with hardcoded
|
|
# URLs that mustn't change).
|
|
|
|
# Field-name suffixes treated as host-bound. Compared after stripping the
|
|
# CFG_<APP>_ prefix.
|
|
_MIGRATE_HOST_BOUND_SUFFIXES=(URL HOST DOMAIN DOMAIN_PREFIX HOSTNAME PUBLIC_URL)
|
|
|
|
# Echo "true" or "false" — should we rewrite host-bound URLs for this app?
|
|
# Defaults true unless the app explicitly opts out.
|
|
migrateUrlRewriteEnabled()
|
|
{
|
|
local app="$1"
|
|
local template_config="$install_containers_dir$app/$app.config"
|
|
[[ ! -f "$template_config" ]] && { echo "true"; return; }
|
|
|
|
local opt_key="CFG_${app^^}_MIGRATE_URL_REWRITE"
|
|
local val
|
|
val=$(grep -E "^${opt_key}=" "$template_config" 2>/dev/null \
|
|
| head -1 | cut -d'=' -f2- | tr -d '"' | tr -d "'")
|
|
if [[ "$val" == "false" || "$val" == "no" || "$val" == "0" ]]; then
|
|
echo "false"
|
|
else
|
|
echo "true"
|
|
fi
|
|
}
|
|
|
|
# Read the destination's install template, find host-bound CFG_<APP>_* keys,
|
|
# and overwrite the same keys in the deployed (just-restored) config with the
|
|
# template's values. Anything not in the host-bound list is left alone — the
|
|
# source's data/preferences survive.
|
|
#
|
|
# Args: <app_name>
|
|
# Returns: count of fields rewritten (emitted as `migrate.url_rewrite.fields`).
|
|
migrateApplyUrlRewrite()
|
|
{
|
|
local app="$1"
|
|
[[ -z "$app" ]] && { isError "migrateApplyUrlRewrite: app required"; return 1; }
|
|
|
|
if [[ "$(migrateUrlRewriteEnabled "$app")" != "true" ]]; then
|
|
isNotice "URL rewrite disabled by app config — keeping source URLs for $app"
|
|
migrateEmit phase=url-rewrite status=skipped reason=app-opt-out app="$app"
|
|
return 0
|
|
fi
|
|
|
|
local template="$install_containers_dir$app/$app.config"
|
|
local deployed="$containers_dir$app/$app.config"
|
|
|
|
if [[ ! -f "$template" || ! -f "$deployed" ]]; then
|
|
isNotice "URL rewrite: missing template or deployed config for $app — skipping"
|
|
migrateEmit phase=url-rewrite status=skipped reason=missing-config app="$app"
|
|
return 0
|
|
fi
|
|
|
|
local rewritten=0
|
|
local key value bare_suffix
|
|
while IFS='=' read -r key value; do
|
|
[[ -z "$key" ]] && continue
|
|
[[ "$key" =~ ^# ]] && continue
|
|
[[ ! "$key" =~ ^CFG_ ]] && continue
|
|
|
|
bare_suffix="${key##*_}"
|
|
# Also catch the two-word _DOMAIN_PREFIX / _PUBLIC_URL cases.
|
|
local two_word="${key%_*}"
|
|
two_word="${two_word##*_}_${bare_suffix}"
|
|
|
|
local is_host_bound=0
|
|
local suffix
|
|
for suffix in "${_MIGRATE_HOST_BOUND_SUFFIXES[@]}"; do
|
|
if [[ "$bare_suffix" == "$suffix" || "$two_word" == "$suffix" ]]; then
|
|
is_host_bound=1
|
|
break
|
|
fi
|
|
done
|
|
(( is_host_bound )) || continue
|
|
|
|
# Strip surrounding quotes; keep whatever the template specifies, even
|
|
# if empty (a deliberately-blank URL is a valid state).
|
|
value="${value%\"}"; value="${value#\"}"
|
|
value="${value%\'}"; value="${value#\'}"
|
|
|
|
updateConfigOption "$key" "$value" "$deployed" >/dev/null 2>&1
|
|
((rewritten++))
|
|
migrateEmit phase=url-rewrite status=field app="$app" key="$key" new_value="$value"
|
|
done < <(grep -E '^CFG_' "$template" 2>/dev/null)
|
|
|
|
isSuccessful "URL rewrite: $rewritten host-bound field(s) repointed to this host for $app"
|
|
migrateEmit phase=url-rewrite status=complete app="$app" fields="$rewritten"
|
|
}
|