1. Secret apply conflict: secret.yaml is a raw `kubectl get -o yaml` dump
that includes the live resourceVersion/uid/creationTimestamp at capture
time. Applying a stale resourceVersion trips optimistic-concurrency
control ("the object has been modified") on effectively every restore,
since normal cluster activity keeps bumping it. Strip those three
volatile fields before `kubectl apply`.
2. Wrong re-seed source for Nextcloud: the re-seed step added for the PVC
exclusion fix assumed both Mautic and Nextcloud bake their excluded dirs
in at their own PVC mount path (/var/www/html), copying from
pvc_path/$dir. True for Mautic, wrong for Nextcloud — its image keeps
the shipped source tree at /usr/src/nextcloud, completely separate from
/var/www/html (empty in the raw image at that path). Caught in testing
("cp: cannot stat '/var/www/html/apps'") because the reseed_failed gate
from the previous fix did its job and left the app at 0 replicas instead
of booting broken — but still required a live manual fix to bring
Nextcloud back after the failed test. New RECONSTRUCT_SRC table makes
the copy source explicit and independently verified per app instead of
assumed by analogy: confirmed /usr/src/nextcloud/{apps,core,dist,
3rdparty,lib} exist in a bare nextcloud:32 pod with no volume mounted
before relying on it.
Verified end-to-end after both fixes: fresh Nextcloud backup -> restore ->
data/ intact (184M, 89 files, unchanged) -> occ status installed/healthy ->
200 on /status.php, via the actual script run (not just the manual
recovery), confirming the automated path works, not just my live fix.
403 lines
21 KiB
Bash
Executable File
403 lines
21 KiB
Bash
Executable File
#!/bin/bash
|
|
# =============================================
|
|
# restore-k8s-apps.sh — k8s-native replacement for restore-myapps.sh
|
|
# Restores: n8n, Odoo, Mautic, Nextcloud, Frappe/ERPNext (all live in k3s)
|
|
#
|
|
# Contract mirrors restore-myapps.sh exactly, so wiring into app.py's
|
|
# restore_start route is a pure path swap:
|
|
# - Expects to be run from inside an already-extracted backup directory
|
|
# (SCRIPT_DIR = the script's own directory), i.e. app.py's existing
|
|
# "tar -xzf backup --strip-components=1 && cp restore-*.sh <session> &&
|
|
# cd <session> && bash restore-*.sh --apps X" flow works unchanged —
|
|
# only the two script filenames/paths need to change on the Python side.
|
|
# - Same --apps flag: comma-separated subset of frappe,odoo,nextcloud,mautic,n8n
|
|
# (omit = restore all found in the backup).
|
|
#
|
|
# Restore order per app (migration-status.md §5 — never populate a PVC
|
|
# that's still mounted by a running consumer; always populate-then-deploy):
|
|
# 1. kubectl apply the Secret + manifests (idempotent — creates if missing,
|
|
# updates in place if present; this is also the DR-onto-fresh-cluster path
|
|
# for anything that isn't a PVC, which pre-exists there already this session)
|
|
# 2. Scale the app Deployment to 0 and wait for the pod to terminate
|
|
# 3. Populate the app-data PVC via a short-lived loader pod + kubectl exec
|
|
# (never raw filesystem/PVC-path access)
|
|
# 4. Scale the app Deployment back up
|
|
# 5. Restore the DB via pg_restore/mysql (SQL-level, never a raw datadir copy)
|
|
# against the DB Deployment, which stays running throughout
|
|
#
|
|
# NOT included this session (explicitly deferred, see backup-migration-plan.md §5):
|
|
# - Cross-cluster / disaster-recovery restore. This script assumes the target
|
|
# cluster/namespaces/PVCs already exist (same-cluster restore-in-place).
|
|
# A fresh-cluster bootstrap + PVC-from-scratch path is future work.
|
|
# =============================================
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# When run inside the management-platform pod, only the static k3s binary is
|
|
# hostPath-mounted (no separate kubectl binary) — fall back to `k3s kubectl`.
|
|
# IMPORTANT: this pod also has a pre-existing rw hostPath mount of /root (for
|
|
# unrelated file-browsing features), which means the host's own root-owned
|
|
# ~/.kube/config (full cluster-admin) is silently visible inside the pod too.
|
|
# `--kubeconfig=/dev/null` is REQUIRED here to stop k3s kubectl from picking
|
|
# that up as a base config — without it, every command below silently runs
|
|
# as cluster-admin instead of the scoped management-platform-sa token,
|
|
# defeating the RBAC in management-platform-backup-rbac.yaml entirely.
|
|
if ! command -v kubectl &>/dev/null && command -v k3s &>/dev/null; then
|
|
kubectl() {
|
|
k3s kubectl \
|
|
--kubeconfig=/dev/null \
|
|
--server=https://kubernetes.default.svc \
|
|
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
|
|
--token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
|
|
"$@"
|
|
}
|
|
fi
|
|
|
|
APPS_FILTER="all"
|
|
VALID_APPS="frappe odoo nextcloud mautic n8n"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--apps)
|
|
APPS_FILTER="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ "$APPS_FILTER" != "all" ]; then
|
|
APPS_FILTER=$(echo "$APPS_FILTER" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
IFS=',' read -ra _APP_CHECK <<< "$APPS_FILTER"
|
|
for _a in "${_APP_CHECK[@]}"; do
|
|
if [[ " $VALID_APPS " != *" $_a "* ]]; then
|
|
echo "❌ Unknown app '$_a' in --apps. Valid options: $VALID_APPS"
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
app_selected() {
|
|
local app="$1"
|
|
[ "$APPS_FILTER" = "all" ] && return 0
|
|
[[ ",$APPS_FILTER," == *",${app},"* ]]
|
|
}
|
|
|
|
# --------------------------------------------------
|
|
# Per-app metadata table (same shape as backup-k8s-apps.sh)
|
|
# --------------------------------------------------
|
|
declare -A NS=( [n8n]=n8n [odoo]=odoo [mautic]=mautic [nextcloud]=nextcloud [frappe]=erpnext )
|
|
declare -A APP_DEPLOY=( [n8n]=n8n [odoo]=odoo [mautic]=mautic [nextcloud]=nextcloud [frappe]=erpnext-web )
|
|
declare -A DB_DEPLOY=( [n8n]=n8n-postgres [odoo]=odoo-postgres [mautic]=mautic-mariadb [nextcloud]=nextcloud-postgres [frappe]=erpnext-mariadb )
|
|
declare -A DB_ENGINE=( [n8n]=postgres [odoo]=postgres [mautic]=mariadb [nextcloud]=postgres [frappe]=mariadb )
|
|
declare -A DB_NAME=( [n8n]=n8n [odoo]=odoo [mautic]=mautic [nextcloud]=nextcloud [frappe]=DYNAMIC )
|
|
declare -A PVC_NAME=( [n8n]=n8n-data [odoo]=odoo-data [mautic]=mautic-app-data [nextcloud]=nextcloud-app-data [frappe]=erpnext-sites )
|
|
declare -A PVC_PATH=( [n8n]="/home/node/.n8n" [odoo]="/var/lib/odoo" [mautic]="/var/www/html" [nextcloud]="/var/www/html" [frappe]="/home/frappe/frappe-bench/sites" )
|
|
declare -A SECRET_NAME=( [n8n]=n8n-secrets [odoo]=odoo-secrets [mautic]=mautic-secrets [nextcloud]=nextcloud-secrets [frappe]=erpnext-secrets )
|
|
declare -A DB_USER_KEY=( [n8n]=DB_POSTGRESDB_USER [odoo]=POSTGRES_USER [mautic]=MYSQL_USER [nextcloud]=POSTGRES_USER [frappe]="" )
|
|
declare -A DB_PASS_KEY=( [n8n]=DB_POSTGRESDB_PASSWORD [odoo]=POSTGRES_PASSWORD [mautic]=MYSQL_ROOT_PASSWORD [nextcloud]=POSTGRES_PASSWORD [frappe]=MARIADB_ROOT_PASSWORD )
|
|
declare -A DOMAIN=( [n8n]="n8nwf.nav.ovh" [odoo]="odooo.nav.ovh" [mautic]="mautics.nav.ovh" [nextcloud]="next.cloud.nav.ovh" [frappe]="erpnext.navitrends.ovh" )
|
|
|
|
# Dirs backup-k8s-apps.sh excludes from pvc-data.tar.gz for these two apps
|
|
# (reproducible framework/vendor code, not user data — see matching
|
|
# TAR_EXCLUDES table + comment there). They must be re-seeded here from the
|
|
# app's own live image, NOT reconstructed by the container entrypoint:
|
|
# - Mautic's image has no logic at all to repopulate node_modules/vendor
|
|
# if missing — confirmed empty by reading /entrypoint.sh.
|
|
# - Nextcloud's entrypoint only rsyncs from /usr/src/nextcloud when it
|
|
# detects image_version > installed_version (via version.php). Since
|
|
# version.php itself is NOT excluded (kept, "everything else"), a
|
|
# restored version.php already matches the running image's version, so
|
|
# that rsync path never fires — relying on it would silently leave
|
|
# apps/core/dist/3rdparty/lib missing after restore.
|
|
# Both images bake these dirs into their own filesystem layer independent
|
|
# of any volume mount, so cp'ing from a loader pod running the same image
|
|
# (PVC mounted elsewhere, so it doesn't shadow the image's own copy) is
|
|
# reliable regardless of either app's startup logic.
|
|
#
|
|
# RECONSTRUCT_SRC is the path *inside the image* to copy from — NOT
|
|
# necessarily pvc_path. Verified per app by running the actual image with no
|
|
# volume mounted and checking where it bakes the dirs in:
|
|
# - Mautic bakes node_modules/vendor directly at /var/www/html (its own
|
|
# PVC mount path) — same path, source == destination parent.
|
|
# - Nextcloud's image keeps its shipped source tree at /usr/src/nextcloud
|
|
# (confirmed via `du -sh` in a bare nextcloud:32 pod), completely
|
|
# separate from /var/www/html (the PVC mount, empty at that path in the
|
|
# image). Assuming pvc_path here (mirroring Mautic's layout) was wrong
|
|
# and failed loudly in testing — cp: cannot stat '/var/www/html/apps' —
|
|
# caught by the reseed_failed gate above rather than silently booting
|
|
# nextcloud broken.
|
|
declare -A RECONSTRUCT_DIRS=( [mautic]="node_modules vendor" [nextcloud]="apps core dist 3rdparty lib" )
|
|
declare -A RECONSTRUCT_SRC=( [mautic]="/var/www/html" [nextcloud]="/usr/src/nextcloud" )
|
|
|
|
ALL_APPS="n8n odoo mautic nextcloud frappe"
|
|
LOADER_IMAGE="alpine:3.20"
|
|
|
|
echo "========================================="
|
|
echo "🔄 k8s Restore — SAME-CLUSTER MODE"
|
|
echo " Backup dir: $SCRIPT_DIR"
|
|
if [ "$APPS_FILTER" = "all" ]; then
|
|
echo " Apps: ALL (found in backup)"
|
|
else
|
|
echo " Apps: $APPS_FILTER"
|
|
fi
|
|
echo "========================================="
|
|
|
|
check_resources() {
|
|
local avail_mb avail_gb
|
|
# /proc/meminfo instead of `free` — `free` isn't present in the
|
|
# management-platform pod's minimal image, /proc/meminfo always is.
|
|
avail_mb=$(awk '/^MemAvailable:/{printf "%d", $2/1024}' /proc/meminfo)
|
|
avail_gb=$(df -BG / | awk 'NR==2{gsub("G","",$4); print $4}')
|
|
# Threshold lowered 1536->1024MB 2026-08-21 — see backup-k8s-apps.sh
|
|
# check_resources() comment for the investigation (baseline host RAM,
|
|
# not a leak).
|
|
if [ "$avail_mb" -lt 1024 ] || [ "$avail_gb" -lt 10 ]; then
|
|
echo " ❌ Resource safety threshold hit (RAM=${avail_mb}MB, Disk=${avail_gb}GB) — aborting."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
RESTORED_APPS=()
|
|
FAILED_APPS=()
|
|
|
|
check_resources
|
|
|
|
for app in $ALL_APPS; do
|
|
app_selected "$app" || continue
|
|
|
|
if [ ! -d "$SCRIPT_DIR/$app" ]; then
|
|
echo ""
|
|
echo " ⏭️ $app — not present in this backup, skipping"
|
|
continue
|
|
fi
|
|
|
|
ns="${NS[$app]}"
|
|
app_deploy="${APP_DEPLOY[$app]}"
|
|
db_deploy="${DB_DEPLOY[$app]}"
|
|
db_engine="${DB_ENGINE[$app]}"
|
|
db_name="${DB_NAME[$app]}"
|
|
pvc_name="${PVC_NAME[$app]}"
|
|
pvc_path="${PVC_PATH[$app]}"
|
|
secret_name="${SECRET_NAME[$app]}"
|
|
db_user_key="${DB_USER_KEY[$app]}"
|
|
db_pass_key="${DB_PASS_KEY[$app]}"
|
|
APP_DIR="$SCRIPT_DIR/$app"
|
|
|
|
echo ""
|
|
echo "-----------------------------------------"
|
|
echo "🔧 [$app] namespace=$ns"
|
|
echo "-----------------------------------------"
|
|
|
|
app_ok=true
|
|
|
|
# ---- 1. Secret (apply — idempotent update, no create: every Secret
|
|
# restored here already exists in a same-cluster restore) ----
|
|
if [ -f "$APP_DIR/secret.yaml" ]; then
|
|
echo -n " 🔑 Applying Secret ... "
|
|
# secret.yaml is a raw `kubectl get -o yaml` dump, which includes the
|
|
# live resourceVersion/uid/creationTimestamp at capture time. Passing
|
|
# a stale resourceVersion to `apply` trips optimistic-concurrency
|
|
# control ("the object has been modified") on any restore where the
|
|
# secret's real resourceVersion has since moved on — which is every
|
|
# restore, since normal cluster activity (this backup pipeline's own
|
|
# earlier applies included) bumps it constantly. Strip the volatile
|
|
# fields before applying; they're server-assigned and never meant to
|
|
# round-trip through a backup.
|
|
sed -e '/^ resourceVersion:/d' -e '/^ uid:/d' -e '/^ creationTimestamp:/d' \
|
|
"$APP_DIR/secret.yaml" > "$APP_DIR/secret.apply.yaml"
|
|
kubectl apply -f "$APP_DIR/secret.apply.yaml" &>/dev/null && echo "✅" || { echo "⚠️ FAILED"; app_ok=false; }
|
|
fi
|
|
# NOTE: manifests.yaml (Deployment/Service/ConfigMap/Ingress specs) is
|
|
# captured by backup-k8s-apps.sh but deliberately NOT re-applied here.
|
|
# Same-cluster restore-in-place never needs to reconcile those specs —
|
|
# only Secret values, PVC data, and DB content actually change — and
|
|
# the pod's RBAC (management-platform-backup-role) intentionally grants
|
|
# no write on Deployments/Services/ConfigMaps/Ingresses beyond the
|
|
# deployments/scale subresource used in step 2/4 below. manifests.yaml
|
|
# is kept in every backup purely as a captured reference for a future
|
|
# fresh-cluster/DR restore path, which is explicitly out of scope today.
|
|
|
|
# ---- 2. Scale app down ----
|
|
prior_replicas=$(kubectl get deployment "$app_deploy" -n "$ns" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo 1)
|
|
[ -z "$prior_replicas" ] && prior_replicas=1
|
|
echo -n " ⏸️ Scaling $app_deploy to 0 ... "
|
|
kubectl scale deployment "$app_deploy" -n "$ns" --replicas=0 &>/dev/null \
|
|
&& kubectl wait --for=delete pod -l app="$app" -n "$ns" --timeout=90s &>/dev/null
|
|
echo "✅"
|
|
|
|
# ---- 3. Restore PVC data via loader pod ----
|
|
if [ -f "$APP_DIR/pvc-data.tar.gz" ]; then
|
|
if kubectl get pvc "$pvc_name" -n "$ns" &>/dev/null; then
|
|
loader="${app}-restore-loader"
|
|
echo -n " 📦 Populating PVC $pvc_name via loader pod ... "
|
|
kubectl run "$loader" -n "$ns" --image="$LOADER_IMAGE" --restart=Never \
|
|
--overrides="{\"spec\":{\"containers\":[{\"name\":\"loader\",\"image\":\"${LOADER_IMAGE}\",\"command\":[\"sleep\",\"3600\"],\"volumeMounts\":[{\"name\":\"data\",\"mountPath\":\"/restore\"}]}],\"volumes\":[{\"name\":\"data\",\"persistentVolumeClaim\":{\"claimName\":\"${pvc_name}\"}}]}}" \
|
|
&>/dev/null
|
|
kubectl wait --for=condition=Ready "pod/$loader" -n "$ns" --timeout=60s &>/dev/null
|
|
|
|
kubectl exec -n "$ns" "$loader" -- sh -c 'rm -rf /restore/* /restore/.[!.]* /restore/..?* 2>/dev/null; true' &>/dev/null
|
|
if kubectl exec -i -n "$ns" "$loader" -- tar xzf - -C /restore < "$APP_DIR/pvc-data.tar.gz" &>"$APP_DIR/restore-pvc.err"; then
|
|
echo "✅"
|
|
rm -f "$APP_DIR/restore-pvc.err"
|
|
else
|
|
echo "⚠️ FAILED (see $APP_DIR/restore-pvc.err)"
|
|
app_ok=false
|
|
fi
|
|
kubectl delete pod "$loader" -n "$ns" --wait=false &>/dev/null
|
|
else
|
|
echo " ⚠️ PVC '$pvc_name' does not exist in this cluster — skipping PVC restore"
|
|
echo " (fresh-cluster/DR provisioning of PVCs from manifests is not yet implemented)"
|
|
app_ok=false
|
|
fi
|
|
else
|
|
echo " ⏭️ No pvc-data.tar.gz in backup — skipping PVC restore"
|
|
fi
|
|
|
|
# ---- 3b. Re-seed reproducible dirs excluded from backup (see
|
|
# RECONSTRUCT_DIRS comment above) from the app's own live image,
|
|
# never from the backup archive itself ----
|
|
reseed_failed=false
|
|
if [ -n "${RECONSTRUCT_DIRS[$app]:-}" ] && kubectl get pvc "$pvc_name" -n "$ns" &>/dev/null; then
|
|
app_image=$(kubectl get deployment "$app_deploy" -n "$ns" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null)
|
|
if [ -n "$app_image" ]; then
|
|
seed_loader="${app}-reseed-loader"
|
|
echo -n " 🧩 Re-seeding [${RECONSTRUCT_DIRS[$app]}] from $app_image ... "
|
|
# imagePullPolicy IfNotPresent: $app_image is read straight off the
|
|
# currently-running Deployment, so it's already on this node —
|
|
# forcing a re-pull (the ":latest"/floating-tag default) just adds
|
|
# a slow, needless network fetch during every restore.
|
|
kubectl run "$seed_loader" -n "$ns" --image="$app_image" --restart=Never \
|
|
--overrides="{\"spec\":{\"containers\":[{\"name\":\"loader\",\"image\":\"${app_image}\",\"imagePullPolicy\":\"IfNotPresent\",\"command\":[\"sleep\",\"3600\"],\"volumeMounts\":[{\"name\":\"data\",\"mountPath\":\"/restore\"}]}],\"volumes\":[{\"name\":\"data\",\"persistentVolumeClaim\":{\"claimName\":\"${pvc_name}\"}}]}}" \
|
|
&>/dev/null
|
|
# 240s, not 60/90s like the alpine loader above: this pulls the
|
|
# full app image (600MB+ for mautic/nextcloud) if it isn't cached
|
|
# on this node yet — a 90s timeout was observed to fire mid-pull
|
|
# in testing, silently leaving the app pod without its
|
|
# reconstructed dirs after being scaled back up.
|
|
if kubectl wait --for=condition=Ready "pod/$seed_loader" -n "$ns" --timeout=240s &>/dev/null; then
|
|
seed_ok=true
|
|
: > "$APP_DIR/reseed.err"
|
|
seed_src="${RECONSTRUCT_SRC[$app]}"
|
|
for _seed_dir in ${RECONSTRUCT_DIRS[$app]}; do
|
|
kubectl exec -n "$ns" "$seed_loader" -- sh -c "rm -rf '/restore/$_seed_dir' && cp -a '${seed_src}/${_seed_dir}' '/restore/$_seed_dir'" \
|
|
>>"$APP_DIR/reseed.err" 2>&1 || seed_ok=false
|
|
done
|
|
if $seed_ok; then
|
|
echo "✅"
|
|
rm -f "$APP_DIR/reseed.err"
|
|
else
|
|
echo "⚠️ FAILED (see $APP_DIR/reseed.err)"
|
|
app_ok=false
|
|
reseed_failed=true
|
|
fi
|
|
else
|
|
echo "⚠️ loader pod not ready"
|
|
app_ok=false
|
|
reseed_failed=true
|
|
fi
|
|
kubectl delete pod "$seed_loader" -n "$ns" --wait=false &>/dev/null
|
|
else
|
|
echo " ⚠️ Could not determine $app image — skipping re-seed of [${RECONSTRUCT_DIRS[$app]}]"
|
|
app_ok=false
|
|
reseed_failed=true
|
|
fi
|
|
fi
|
|
|
|
# ---- 4. Scale app back up ----
|
|
# Never bring the app back with replicas>0 if a required re-seed (3b)
|
|
# failed — for Mautic/Nextcloud that means booting with vendor/ or
|
|
# core/lib missing, which is a hard crash/broken-app state, not a
|
|
# degraded one. Left at 0 so the operator fixes it and scales up
|
|
# manually rather than the restore silently serving a broken app.
|
|
if $reseed_failed; then
|
|
echo " ⛔ Skipping scale-up — required re-seed failed (app left at 0 replicas, see above)"
|
|
else
|
|
echo -n " ▶️ Scaling $app_deploy back to $prior_replicas ... "
|
|
kubectl scale deployment "$app_deploy" -n "$ns" --replicas="$prior_replicas" &>/dev/null && echo "✅" || echo "⚠️ FAILED"
|
|
fi
|
|
|
|
# ---- 5. DB restore (SQL-level, never raw datadir copy) ----
|
|
if [ -f "$APP_DIR/db-dump.sql.gz" ]; then
|
|
echo -n " 💾 Restoring DB ($db_engine) ... "
|
|
case "$db_engine" in
|
|
postgres)
|
|
db_user=$(kubectl get secret "$secret_name" -n "$ns" -o jsonpath="{.data.${db_user_key}}" 2>/dev/null | base64 -d)
|
|
db_pass=$(kubectl get secret "$secret_name" -n "$ns" -o jsonpath="{.data.${db_pass_key}}" 2>/dev/null | base64 -d)
|
|
if [ -n "$db_user" ] && [ -n "$db_pass" ]; then
|
|
kubectl exec -n "$ns" "deploy/${db_deploy}" -- env PGPASSWORD="$db_pass" \
|
|
psql -U "$db_user" -d postgres -c "DROP DATABASE IF EXISTS \"$db_name\";" &>/dev/null
|
|
kubectl exec -n "$ns" "deploy/${db_deploy}" -- env PGPASSWORD="$db_pass" \
|
|
createdb -U "$db_user" "$db_name" &>/dev/null
|
|
if gunzip -c "$APP_DIR/db-dump.sql.gz" | \
|
|
kubectl exec -i -n "$ns" "deploy/${db_deploy}" -- env PGPASSWORD="$db_pass" \
|
|
psql -U "$db_user" -d "$db_name" &>"$APP_DIR/restore-db.err"; then
|
|
echo "✅"
|
|
rm -f "$APP_DIR/restore-db.err"
|
|
else
|
|
echo "⚠️ FAILED (see $APP_DIR/restore-db.err)"
|
|
app_ok=false
|
|
fi
|
|
else
|
|
echo "⚠️ could not read DB credentials from (restored) secret"
|
|
app_ok=false
|
|
fi
|
|
;;
|
|
mariadb)
|
|
if [ "$db_name" = "DYNAMIC" ]; then
|
|
db_name=$(cat "$APP_DIR/db-name.txt" 2>/dev/null)
|
|
fi
|
|
db_pass=$(kubectl get secret "$secret_name" -n "$ns" -o jsonpath="{.data.${db_pass_key}}" 2>/dev/null | base64 -d)
|
|
if [ -n "$db_name" ] && [ -n "$db_pass" ]; then
|
|
kubectl exec -n "$ns" "deploy/${db_deploy}" -- env MYSQL_PWD="$db_pass" \
|
|
mysql -uroot -e "DROP DATABASE IF EXISTS \`$db_name\`;" &>/dev/null
|
|
if gunzip -c "$APP_DIR/db-dump.sql.gz" | \
|
|
kubectl exec -i -n "$ns" "deploy/${db_deploy}" -- env MYSQL_PWD="$db_pass" \
|
|
mysql -uroot &>"$APP_DIR/restore-db.err"; then
|
|
echo "✅ (db=$db_name)"
|
|
rm -f "$APP_DIR/restore-db.err"
|
|
else
|
|
echo "⚠️ FAILED (see $APP_DIR/restore-db.err)"
|
|
app_ok=false
|
|
fi
|
|
else
|
|
echo "⚠️ could not determine DB name/credentials"
|
|
app_ok=false
|
|
fi
|
|
;;
|
|
esac
|
|
else
|
|
echo " ⏭️ No db-dump.sql.gz in backup — skipping DB restore"
|
|
fi
|
|
|
|
if [ "$app_ok" = true ]; then
|
|
RESTORED_APPS+=("$app")
|
|
else
|
|
FAILED_APPS+=("$app")
|
|
fi
|
|
|
|
check_resources
|
|
done
|
|
|
|
# --------------------------------------------------
|
|
# Summary
|
|
# --------------------------------------------------
|
|
echo ""
|
|
echo "========================================="
|
|
echo "✅ RESTORE COMPLETE"
|
|
echo "========================================="
|
|
for app in "${RESTORED_APPS[@]:-}"; do
|
|
[ -z "$app" ] && continue
|
|
echo " ${app} → https://${DOMAIN[$app]}"
|
|
done
|
|
if [ "${#FAILED_APPS[@]}" -gt 0 ]; then
|
|
echo ""
|
|
echo " ⚠️ Had failures: ${FAILED_APPS[*]} — check the .err files under each app's backup dir"
|
|
fi
|
|
echo "========================================="
|