feat(tooling): host migration scripts (export/import/check)
Move the stack to a new host keeping the same domain, carrying Drasl users + skins (+ kuma monitors, filestash index) but not the world. - migrate-check.sh: validate the target meets requirements. [restore] hard-fails (docker, compose v2, git, tar, ssh); [deploy] warns (node/pnpm, py>=3.11, nginx, acme.sh, RAM, disk, ports). --host runs it remotely by streaming itself over ssh. - migrate-export.sh: workstation-run, pulls the source read-only over ssh into one 0600 bundle (.env, certs/, drasl_state/kuma_data/ filestash_state volumes, checksummed MANIFEST). Certs in by default (--without-certs); world opt-in (--with-world). - migrate-import.sh: target-run, sha256-verify, refuse existing volumes (--force), backup+restore .env with --path-prefix rewrite, check host paths resolve, restore certs+volumes, print the manual tail. - plan/22-migration.md: human walkthrough (cochi -> oxps), rollback window warning, rehearsal notes. - .gitignore: ignore ulicraft-bundle-*.tar.gz (holds secrets + LE keys). Assumes Compose v2 on the target (no update-all.sh changes). Distribution repo, TLS issuance, DNS and firewall stay manual. Round-trip tested against prod: 9.2M bundle, drasl.db restored byte-exact, clobber guard verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
00e9815307
commit
b9bcc13b73
5 files changed
+625
No files matched your search
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
# migrate-check.sh — validate that a machine meets the requirements to RESTORE
|
||||
# and RUN the Ulicraft stack. Run it on the TARGET host (e.g. oxps) before a
|
||||
# migration, or against a remote host with --host.
|
||||
#
|
||||
# Two tiers of checks:
|
||||
# [restore] hard requirements — without these migrate-import.sh cannot run.
|
||||
# Any failure exits non-zero.
|
||||
# [deploy] needed to build + serve the full stack (update-all.sh, TLS).
|
||||
# Missing ones WARN only — you can restore now, install later.
|
||||
#
|
||||
# Usage:
|
||||
# tooling/migrate-check.sh # check THIS machine
|
||||
# tooling/migrate-check.sh --host oxps # check a remote host over ssh
|
||||
#
|
||||
# The --host form streams this very script to the remote and runs it there, so
|
||||
# nothing needs to be installed on the remote first.
|
||||
set -uo pipefail # NOT -e: we want to run every check and total the failures
|
||||
|
||||
# ── remote mode: ship self to the host and run it there ─────────────
|
||||
if [ "${1:-}" = "--host" ]; then
|
||||
[ -n "${2:-}" ] || { echo "usage: $0 --host <ssh-host>" >&2; exit 2; }
|
||||
exec ssh "$2" 'bash -s' < "$0"
|
||||
fi
|
||||
|
||||
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
ok() { printf ' \033[1;32m✓\033[0m %s\n' "$*"; }
|
||||
bad() { printf ' \033[1;31m✗\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[1;33m!\033[0m %s\n' "$*"; }
|
||||
|
||||
HARD_FAIL=0
|
||||
SOFT_FAIL=0
|
||||
|
||||
# require CMD "human hint" — hard [restore] check
|
||||
require() {
|
||||
if command -v "$1" >/dev/null 2>&1; then ok "$1 present"; else
|
||||
bad "$1 MISSING — $2"; HARD_FAIL=$((HARD_FAIL+1)); fi
|
||||
}
|
||||
# want CMD "human hint" — soft [deploy] check
|
||||
want() {
|
||||
if command -v "$1" >/dev/null 2>&1; then ok "$1 present"; else
|
||||
warn "$1 missing — $2"; SOFT_FAIL=$((SOFT_FAIL+1)); fi
|
||||
}
|
||||
|
||||
log "[restore] core tools"
|
||||
require docker "install Docker Engine"
|
||||
require git "install git"
|
||||
require tar "install tar"
|
||||
require gzip "install gzip"
|
||||
require ssh "install openssh-client (used to pull the bundle)"
|
||||
|
||||
log "[restore] docker daemon reachable"
|
||||
if docker info >/dev/null 2>&1; then ok "docker daemon up"; else
|
||||
bad "cannot talk to the docker daemon (permission? is it running?)"; HARD_FAIL=$((HARD_FAIL+1)); fi
|
||||
|
||||
log "[restore] docker compose v2 plugin"
|
||||
# The stack is authored for Compose v2 (docker compose ...). The legacy v1
|
||||
# docker-compose binary is NOT accepted — it mangles the project name and
|
||||
# diverges on --build.
|
||||
if cv="$(docker compose version --short 2>/dev/null)"; then
|
||||
major="${cv%%.*}"
|
||||
if [ "${major:-0}" -ge 2 ] 2>/dev/null; then
|
||||
ok "docker compose v$cv"
|
||||
else
|
||||
bad "docker compose reports v$cv — need v2+"; HARD_FAIL=$((HARD_FAIL+1))
|
||||
fi
|
||||
else
|
||||
bad "'docker compose' v2 plugin not found. Install it:
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-\$(uname -m) \\
|
||||
-o ~/.docker/cli-plugins/docker-compose
|
||||
chmod +x ~/.docker/cli-plugins/docker-compose
|
||||
(the legacy 'docker-compose' v1 binary is NOT accepted)"
|
||||
HARD_FAIL=$((HARD_FAIL+1))
|
||||
fi
|
||||
|
||||
log "[deploy] build + config tools"
|
||||
want envsubst "part of gettext — render-config.sh / render-nginx.sh need it"
|
||||
want rsync "used to publish the distribution repo"
|
||||
want openssl "used to mint FILES_PASSWORD_HASH"
|
||||
want jq "sync-server-mods.sh falls back to python3 if absent"
|
||||
want node "landing build (pnpm build) — install via nvm"
|
||||
want pnpm "landing build — never npm (see project rules)"
|
||||
|
||||
log "[deploy] python >= 3.11 (build-modlist.py)"
|
||||
PY=""
|
||||
for c in python3.13 python3.12 python3.11 python3; do
|
||||
if command -v "$c" >/dev/null 2>&1; then
|
||||
v="$($c -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null)"
|
||||
case "$v" in 3.1[1-9]|3.[2-9]*) PY="$c $v"; break;; esac
|
||||
fi
|
||||
done
|
||||
if [ -n "$PY" ]; then ok "python $PY"; else
|
||||
warn "no python >= 3.11 found — build-modlist.py will refuse"; SOFT_FAIL=$((SOFT_FAIL+1)); fi
|
||||
|
||||
log "[deploy] TLS front (host nginx + acme.sh)"
|
||||
want nginx "host nginx terminates TLS in front of caddy"
|
||||
if [ -x "$HOME/.acme.sh/acme.sh" ] || command -v acme.sh >/dev/null 2>&1; then
|
||||
ok "acme.sh present"
|
||||
else
|
||||
warn "acme.sh missing — needed to RENEW certs (bundle covers the first ~90d):
|
||||
curl https://get.acme.sh | sh -s email=you@example.com"
|
||||
SOFT_FAIL=$((SOFT_FAIL+1))
|
||||
fi
|
||||
|
||||
log "[deploy] memory (MAX_MEMORY: 10G in compose)"
|
||||
if command -v free >/dev/null 2>&1; then
|
||||
mem_g="$(free -g | awk '/^Mem:/{print $2}')"
|
||||
if [ "${mem_g:-0}" -ge 12 ] 2>/dev/null; then ok "${mem_g}G RAM"; else
|
||||
warn "only ${mem_g}G RAM — the JVM alone wants 10G; lower MAX_MEMORY or add RAM"; SOFT_FAIL=$((SOFT_FAIL+1)); fi
|
||||
else
|
||||
warn "cannot read RAM (no 'free')"; SOFT_FAIL=$((SOFT_FAIL+1))
|
||||
fi
|
||||
|
||||
log "[deploy] free disk on / (need ~10G for images + volumes)"
|
||||
if command -v df >/dev/null 2>&1; then
|
||||
free_g="$(df -BG --output=avail / 2>/dev/null | awk 'NR==2{gsub("G","");print $1}')"
|
||||
if [ "${free_g:-0}" -ge 10 ] 2>/dev/null; then ok "${free_g}G free on /"; else
|
||||
warn "only ${free_g}G free on / — images + volumes want ~10G"; SOFT_FAIL=$((SOFT_FAIL+1)); fi
|
||||
fi
|
||||
|
||||
log "[deploy] ports free (server + ingress)"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
for p in 25565 80 443 "${CADDY_HTTP_PORT:-8880}"; do
|
||||
if ss -ltn "( sport = :$p )" 2>/dev/null | grep -q ":$p"; then
|
||||
warn "tcp/$p already in use — will collide unless it's this stack"; SOFT_FAIL=$((SOFT_FAIL+1))
|
||||
else ok "tcp/$p free"; fi
|
||||
done
|
||||
if ss -lun "( sport = :24454 )" 2>/dev/null | grep -q ":24454"; then
|
||||
warn "udp/24454 already in use (Simple Voice Chat)"; SOFT_FAIL=$((SOFT_FAIL+1))
|
||||
else ok "udp/24454 free"; fi
|
||||
else
|
||||
warn "cannot check ports (no 'ss')"; SOFT_FAIL=$((SOFT_FAIL+1))
|
||||
fi
|
||||
|
||||
# ── verdict ─────────────────────────────────────────────────────────
|
||||
printf '\n'
|
||||
if [ "$HARD_FAIL" -gt 0 ]; then
|
||||
printf '\033[1;31m✗ %d restore-blocking requirement(s) missing — fix before migrate-import.sh\033[0m\n' "$HARD_FAIL"
|
||||
[ "$SOFT_FAIL" -gt 0 ] && printf '\033[1;33m (+%d deploy warning(s))\033[0m\n' "$SOFT_FAIL"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$SOFT_FAIL" -gt 0 ]; then
|
||||
printf '\033[1;33m✓ restore requirements met, but %d deploy warning(s) — you can import now, but\n install the missing pieces before ./update-all.sh --hard\033[0m\n' "$SOFT_FAIL"
|
||||
exit 0
|
||||
fi
|
||||
printf '\033[1;32m✓ all checks passed — ready to migrate\033[0m\n'
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# migrate-export.sh — bundle the Ulicraft stack's PORTABLE state into a single
|
||||
# tarball, ready to restore on another host with migrate-import.sh.
|
||||
#
|
||||
# Runs on YOUR WORKSTATION and pulls everything from the source host over ssh
|
||||
# (default: cochi). Nothing is written on the source; it is read-only there.
|
||||
#
|
||||
# What goes in the bundle:
|
||||
# .env secrets + config (RCON, FILES_* hashes, DRASL_*, OVH)
|
||||
# certs/ Let's Encrypt cert+key per name (opt out: --without-certs)
|
||||
# volume drasl_state accounts, passwords, skins, capes, signing key ← the point
|
||||
# volume kuma_data Uptime Kuma monitors + history
|
||||
# volume filestash_state filestash sqlite + search index
|
||||
# MANIFEST.txt source host, project name, git commit, sha256sums
|
||||
#
|
||||
# Deliberately NOT included (regenerated or you deploy them yourself):
|
||||
# mc_data (the world — opt in with --with-world), backups/, the distribution
|
||||
# repo, generators-data/, www/, server-mods/, rendered configs.
|
||||
#
|
||||
# Usage:
|
||||
# tooling/migrate-export.sh # from cochi, certs in, no world
|
||||
# tooling/migrate-export.sh --from cochi --out ~/bundles
|
||||
# tooling/migrate-export.sh --without-certs # reissue certs on the target instead
|
||||
# tooling/migrate-export.sh --with-world # also carry the world (~15G)
|
||||
#
|
||||
# The output tarball contains private keys → it is written mode 0600. Keep it safe.
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ── args ────────────────────────────────────────────────────────────
|
||||
FROM="cochi"
|
||||
SRC_DIR="/home/ubuntu/mc/ulicraft-server-v1"
|
||||
OUT_DIR="."
|
||||
WITH_CERTS=1
|
||||
WITH_WORLD=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--from) FROM="${2:?}"; shift 2 ;;
|
||||
--src-dir) SRC_DIR="${2:?}"; shift 2 ;;
|
||||
--out) OUT_DIR="${2:?}"; shift 2 ;;
|
||||
--without-certs) WITH_CERTS=0; shift ;;
|
||||
--with-world) WITH_WORLD=1; shift ;;
|
||||
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
|
||||
*) die "unknown arg: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v ssh >/dev/null || die "ssh required"
|
||||
command -v tar >/dev/null || die "tar required"
|
||||
|
||||
# ── discover the source: project name + git commit ─────────────────
|
||||
log "inspecting source host '$FROM' at $SRC_DIR"
|
||||
ssh "$FROM" "test -f '$SRC_DIR/.env'" || die "no .env at $SRC_DIR on $FROM (wrong --src-dir?)"
|
||||
# Compose project name = the source checkout's dir basename (compose default).
|
||||
PROJ="$(ssh "$FROM" "basename '$SRC_DIR'")"
|
||||
[ -n "$PROJ" ] || die "could not resolve compose project name on $FROM"
|
||||
COMMIT="$(ssh "$FROM" "cd '$SRC_DIR' && git rev-parse --short HEAD 2>/dev/null || echo unknown")"
|
||||
warn "source compose project: $PROJ (git $COMMIT)"
|
||||
|
||||
# volumes to carry (bare suffixes; prefixed with $PROJ on the source)
|
||||
VOLS=(drasl_state kuma_data filestash_state)
|
||||
[ "$WITH_WORLD" -eq 1 ] && VOLS+=(mc_data)
|
||||
|
||||
# Verify every volume exists on the source before we start streaming.
|
||||
for v in "${VOLS[@]}"; do
|
||||
ssh "$FROM" "docker volume inspect '${PROJ}_${v}' >/dev/null 2>&1" \
|
||||
|| die "volume ${PROJ}_${v} not found on $FROM — is the stack the one at $SRC_DIR?"
|
||||
done
|
||||
|
||||
# ── staging dir on the workstation ──────────────────────────────────
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/ulicraft-migrate.XXXXXX")"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
# .env
|
||||
log "pulling .env"
|
||||
ssh "$FROM" "cat '$SRC_DIR/.env'" > "$STAGE/.env"
|
||||
[ -s "$STAGE/.env" ] || die ".env came back empty"
|
||||
|
||||
# certs/
|
||||
if [ "$WITH_CERTS" -eq 1 ]; then
|
||||
if ssh "$FROM" "test -d '$SRC_DIR/certs'"; then
|
||||
log "pulling certs/ (Let's Encrypt cert+key per name)"
|
||||
ssh "$FROM" "tar -C '$SRC_DIR' -czf - certs" > "$STAGE/certs.tar.gz"
|
||||
else
|
||||
warn "no certs/ dir on source — skipping (you'll issue certs on the target)"
|
||||
WITH_CERTS=0
|
||||
fi
|
||||
else
|
||||
warn "--without-certs — certs excluded; issue-letsencrypt.sh on the target"
|
||||
fi
|
||||
|
||||
# docker volumes → one gzip'd tar each, streamed over ssh (never staged on source)
|
||||
for v in "${VOLS[@]}"; do
|
||||
log "dumping volume ${PROJ}_${v}"
|
||||
ssh "$FROM" "docker run --rm -v '${PROJ}_${v}':/v alpine tar -C /v -czf - ." \
|
||||
> "$STAGE/vol-${v}.tar.gz"
|
||||
[ -s "$STAGE/vol-${v}.tar.gz" ] || die "volume ${v} dumped empty — aborting"
|
||||
done
|
||||
|
||||
# ── manifest with checksums ─────────────────────────────────────────
|
||||
log "writing MANIFEST.txt"
|
||||
{
|
||||
echo "ulicraft migration bundle"
|
||||
echo "created: $STAMP"
|
||||
echo "source_host: $FROM"
|
||||
echo "source_dir: $SRC_DIR"
|
||||
echo "source_project: $PROJ"
|
||||
echo "git_commit: $COMMIT"
|
||||
echo "with_certs: $WITH_CERTS"
|
||||
echo "with_world: $WITH_WORLD"
|
||||
echo "volumes: ${VOLS[*]}"
|
||||
echo
|
||||
echo "sha256:"
|
||||
( cd "$STAGE" && sha256sum ./.env ./*.tar.gz 2>/dev/null )
|
||||
} > "$STAGE/MANIFEST.txt"
|
||||
|
||||
# ── seal the bundle (0600 — it holds private keys + secrets) ────────
|
||||
OUT="$OUT_DIR/ulicraft-bundle-${FROM}-${STAMP}.tar.gz"
|
||||
log "sealing $OUT"
|
||||
tar -C "$STAGE" -czf "$OUT" .
|
||||
chmod 600 "$OUT"
|
||||
|
||||
SIZE="$(du -h "$OUT" | cut -f1)"
|
||||
log "done — $OUT ($SIZE)"
|
||||
printf ' next: copy it to the target and run tooling/migrate-import.sh %s\n' "$(basename "$OUT")"
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
# migrate-import.sh — restore a bundle made by migrate-export.sh onto THIS host.
|
||||
#
|
||||
# Run it from inside the target repo checkout (the compose project directory).
|
||||
# It restores .env, certs/, and the docker volumes into THIS host's compose
|
||||
# project, then prints the manual steps that finish the cutover.
|
||||
#
|
||||
# It assumes the machine already meets the requirements — run
|
||||
# tooling/migrate-check.sh
|
||||
# first. It does NOT deploy the distribution repo, issue certs, or start the
|
||||
# stack; those stay in your hands (see the printed tail + plan/22-migration.md).
|
||||
#
|
||||
# Usage:
|
||||
# tooling/migrate-import.sh BUNDLE.tar.gz
|
||||
# tooling/migrate-import.sh BUNDLE.tar.gz --path-prefix /home/ubuntu/mc=/home/you/mc
|
||||
# tooling/migrate-import.sh BUNDLE.tar.gz --without-certs
|
||||
# tooling/migrate-import.sh BUNDLE.tar.gz --force # overwrite existing volumes
|
||||
#
|
||||
# --path-prefix OLD=NEW rewrites the source-absolute paths in .env
|
||||
# (DISTRIBUTION_WEB_ROOT, GENERATORS_REPO, GENERATORS_DATA_DIR, FILES_DATA_DIR)
|
||||
# to where they live on this host. Repeatable.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." # target repo root
|
||||
|
||||
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ── args ────────────────────────────────────────────────────────────
|
||||
BUNDLE=""
|
||||
WITH_CERTS=1
|
||||
FORCE=0
|
||||
declare -a PREFIXES=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--without-certs) WITH_CERTS=0; shift ;;
|
||||
--force) FORCE=1; shift ;;
|
||||
--path-prefix) PREFIXES+=("${2:?}"); shift 2 ;;
|
||||
-h|--help) sed -n '2,26p' "$0"; exit 0 ;;
|
||||
-*) die "unknown flag: $1 (see --help)" ;;
|
||||
*) [ -z "$BUNDLE" ] || die "extra arg: $1"; BUNDLE="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BUNDLE" ] || die "usage: $0 BUNDLE.tar.gz [--path-prefix OLD=NEW] [--without-certs] [--force]"
|
||||
[ -f "$BUNDLE" ] || die "bundle not found: $BUNDLE"
|
||||
BUNDLE="$(cd "$(dirname "$BUNDLE")" && pwd)/$(basename "$BUNDLE")" # absolutize
|
||||
|
||||
command -v docker >/dev/null || die "docker required"
|
||||
docker info >/dev/null 2>&1 || die "cannot reach the docker daemon"
|
||||
docker compose version >/dev/null 2>&1 || die "docker compose v2 plugin required (see migrate-check.sh)"
|
||||
[ -f docker-compose.yml ] || die "run this from inside the target repo checkout (no docker-compose.yml here)"
|
||||
|
||||
# ── target compose project name (default: this dir's basename) ──────
|
||||
PROJ="${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}"
|
||||
log "target compose project: $PROJ (in $PWD)"
|
||||
warn "volumes will be restored as ${PROJ}_<name> — this MUST match what 'docker compose' uses here."
|
||||
warn "if that looks wrong, set COMPOSE_PROJECT_NAME in .env and re-run."
|
||||
|
||||
# ── unpack the bundle to a temp staging dir ─────────────────────────
|
||||
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/ulicraft-import.XXXXXX")"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
log "unpacking $(basename "$BUNDLE")"
|
||||
tar -C "$STAGE" -xzf "$BUNDLE"
|
||||
[ -f "$STAGE/MANIFEST.txt" ] || die "no MANIFEST.txt in bundle — is this a ulicraft migration bundle?"
|
||||
|
||||
log "manifest:"
|
||||
sed 's/^/ /' "$STAGE/MANIFEST.txt"
|
||||
|
||||
# ── verify checksums ────────────────────────────────────────────────
|
||||
log "verifying sha256 checksums"
|
||||
( cd "$STAGE" && awk '/^[0-9a-f]{64} /{print}' MANIFEST.txt | sha256sum -c - ) \
|
||||
|| die "checksum mismatch — bundle is corrupt or truncated"
|
||||
|
||||
# volumes present in the bundle
|
||||
mapfile -t VOL_FILES < <(cd "$STAGE" && ls vol-*.tar.gz 2>/dev/null || true)
|
||||
[ "${#VOL_FILES[@]}" -gt 0 ] || die "no volume archives in bundle"
|
||||
|
||||
# ── refuse to clobber existing volumes unless --force ───────────────
|
||||
CLASH=0
|
||||
for f in "${VOL_FILES[@]}"; do
|
||||
name="${f#vol-}"; name="${name%.tar.gz}"
|
||||
if docker volume inspect "${PROJ}_${name}" >/dev/null 2>&1; then
|
||||
warn "volume ${PROJ}_${name} already exists on this host"
|
||||
CLASH=1
|
||||
fi
|
||||
done
|
||||
if [ "$CLASH" -eq 1 ] && [ "$FORCE" -eq 0 ]; then
|
||||
die "existing volumes would be overwritten. Re-run with --force to replace them
|
||||
(their current contents are discarded). If this is the WRONG host, stop now."
|
||||
fi
|
||||
|
||||
# ── restore .env (backup any existing one first) ────────────────────
|
||||
if [ -f .env ]; then
|
||||
BK=".env.bak.pre-migrate.$(date +%Y%m%d-%H%M%S)"
|
||||
warn "existing .env → $BK"
|
||||
cp -a .env "$BK"
|
||||
fi
|
||||
log "restoring .env"
|
||||
cp "$STAGE/.env" .env
|
||||
chmod 600 .env
|
||||
|
||||
# apply --path-prefix rewrites
|
||||
for p in "${PREFIXES[@]}"; do
|
||||
old="${p%%=*}"; new="${p#*=}"
|
||||
[ -n "$old" ] && [ "$old" != "$p" ] || die "bad --path-prefix '$p' (want OLD=NEW)"
|
||||
log "rewriting .env paths: $old -> $new"
|
||||
# only touch the RHS of KEY=... lines; escape / for sed
|
||||
esc_old="$(printf '%s' "$old" | sed 's/[.[\*^$/]/\\&/g')"
|
||||
esc_new="$(printf '%s' "$new" | sed 's/[&/]/\\&/g')"
|
||||
sed -i "s/${esc_old}/${esc_new}/g" .env
|
||||
done
|
||||
|
||||
# ── sanity-check the host-path vars in .env resolve on THIS host ─────
|
||||
log "checking host paths referenced by .env"
|
||||
set -a; . ./.env; set +a
|
||||
MISSING=0
|
||||
for var in DISTRIBUTION_WEB_ROOT GENERATORS_REPO GENERATORS_DATA_DIR FILES_DATA_DIR; do
|
||||
val="${!var:-}"
|
||||
[ -n "$val" ] || { warn "$var unset in .env"; continue; }
|
||||
if [ -e "$val" ]; then
|
||||
printf ' \033[1;32m✓\033[0m %s = %s\n' "$var" "$val"
|
||||
else
|
||||
printf ' \033[1;33m!\033[0m %s = %s (does NOT exist here)\n' "$var" "$val"
|
||||
MISSING=$((MISSING+1))
|
||||
fi
|
||||
done
|
||||
if [ "$MISSING" -gt 0 ]; then
|
||||
warn "$MISSING path(s) above don't exist on this host yet."
|
||||
warn "DISTRIBUTION_WEB_ROOT especially: the minecraft service bind-mounts kubejs"
|
||||
warn "from it and WON'T BOOT until it's populated. Deploy the distribution repo"
|
||||
warn "(and re-run with --path-prefix if the paths belong elsewhere) before starting."
|
||||
fi
|
||||
|
||||
# ── restore certs/ ──────────────────────────────────────────────────
|
||||
if [ "$WITH_CERTS" -eq 1 ] && [ -f "$STAGE/certs.tar.gz" ]; then
|
||||
log "restoring certs/"
|
||||
[ -d certs ] && warn "existing certs/ will be merged/overwritten from the bundle"
|
||||
tar -C . -xzf "$STAGE/certs.tar.gz"
|
||||
elif [ "$WITH_CERTS" -eq 0 ]; then
|
||||
warn "--without-certs — not restoring certs; run tooling/issue-letsencrypt.sh here"
|
||||
else
|
||||
warn "bundle has no certs.tar.gz — run tooling/issue-letsencrypt.sh here"
|
||||
fi
|
||||
|
||||
# ── restore the docker volumes ──────────────────────────────────────
|
||||
for f in "${VOL_FILES[@]}"; do
|
||||
name="${f#vol-}"; name="${name%.tar.gz}"
|
||||
vol="${PROJ}_${name}"
|
||||
log "restoring volume $vol"
|
||||
docker volume create "$vol" >/dev/null
|
||||
if [ "$FORCE" -eq 1 ]; then
|
||||
# wipe first so a --force restore is a clean replace, not a merge
|
||||
docker run --rm -v "$vol":/v alpine sh -c 'rm -rf /v/* /v/..?* /v/.[!.]* 2>/dev/null || true'
|
||||
fi
|
||||
docker run --rm -i -v "$vol":/v alpine tar -C /v -xzf - < "$STAGE/$f"
|
||||
done
|
||||
|
||||
# ── done: print the manual tail ─────────────────────────────────────
|
||||
cat <<EOF
|
||||
|
||||
$(printf '\033[1;32m==>\033[0m volumes + .env + certs restored.')
|
||||
|
||||
Remaining manual steps (see plan/22-migration.md for detail):
|
||||
|
||||
1. Deploy the distribution repo so DISTRIBUTION_WEB_ROOT is populated
|
||||
(the minecraft service bind-mounts kubejs from it; it won't boot empty).
|
||||
Also add this host's Gitea deploy key for that repo.
|
||||
2. Fetch vendored bits: tooling/fetch-authlib.sh tooling/fetch-launcher.sh
|
||||
and (needs WAN) tooling/sync-generators-data.sh
|
||||
3. Host nginx vhost: tooling/render-nginx.sh --install
|
||||
4. Certificates: tooling/issue-letsencrypt.sh
|
||||
(bundle certs boot nginx now, but nothing renews them until you run this)
|
||||
5. Open the firewall: tcp 25565, 80, 443, ${CADDY_HTTP_PORT:-8880} ; udp 24454
|
||||
6. Point DNS ${BASE_DOMAIN:-\$BASE_DOMAIN} + subdomains at THIS host.
|
||||
7. Bring it up: ./update-all.sh --hard
|
||||
8. Re-op players once they've joined: docker compose exec minecraft rcon-cli op <name>
|
||||
|
||||
Rollback note: same domain means DNS points at ONE host. Once a player registers
|
||||
or changes a skin here, this drasl.db diverges from cochi — rolling back to cochi
|
||||
silently loses those changes. Keep the fallback window short.
|
||||
EOF
|
||||
Reference in new issue
Block a user