diff --git a/scripts/docker/type_switcher/swap_docker_type.sh b/scripts/docker/type_switcher/swap_docker_type.sh index 5ad0410..ccfcf9f 100755 --- a/scripts/docker/type_switcher/swap_docker_type.sh +++ b/scripts/docker/type_switcher/swap_docker_type.sh @@ -24,13 +24,17 @@ dockerSwitcherSwap() isHeader "Docker Root/Rootless Switcher" # Switching modes re-maps container UIDs (rootless shifts them by the - # subuid base), so a stateful app's existing data won't line up in the - # new mode. LibrePortal's own control plane is reconciled automatically - # (reconcileDockerOwnership), but app data is not touched — the safe way - # to carry stateful apps across is backup-before / restore-after. + # subuid base), so a stateful app's on-disk data won't line up in the + # new mode and a chown can't carry it. LibrePortal's control plane is + # reconciled automatically (reconcileDockerOwnership); third-party app + # data is carried by an automatic backup-before / restore-after when a + # backup location is enabled. isNotice "Switching Docker mode re-maps container ownership." - isNotice "Back up any stateful apps (databases etc.) BEFORE switching and restore them AFTER." - isNotice "App data is intentionally left untouched here." + if [[ -n "$(resticEnabledLocations)" ]]; then + isNotice "Stateful apps will be backed up now and restored automatically after the switch." + else + isNotice "No backup location enabled — back up stateful apps by hand BEFORE switching and restore AFTER." + fi if [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then if [[ $flag != "cli" ]]; then @@ -50,6 +54,11 @@ dockerSwitcherSwap() fi if [[ "$switch_rooted_choice" == [yY] ]]; then isNotice "Switching to the Rooted Docker now..." + # Back up every app under the old (still-running) mode first; a + # failure aborts the switch before anything changes. + if ! switchMigrateBackupApps "$docker_type"; then + return 1 + fi # Looking at the Rootless Install if [[ $docker_rootless_found == "true" ]]; then dockerComposeDownAllApps rootless; @@ -65,6 +74,7 @@ dockerSwitcherSwap() reconcileDockerOwnership "$CFG_DOCKER_INSTALL_TYPE"; dockerStartAllApps; databaseOptionInsert "docker_type" $CFG_DOCKER_INSTALL_TYPE; + switchMigrateRestoreApps; fi fi fi @@ -87,6 +97,11 @@ dockerSwitcherSwap() fi if [[ "$switch_rootless_choice" == [yY] ]]; then isNotice "Switching to the Rootless Docker now..." + # Back up every app under the old (still-running) mode first; a + # failure aborts the switch before anything changes. + if ! switchMigrateBackupApps "$docker_type"; then + return 1 + fi # Looking at the Rooted Install if [[ $docker_rooted_found == "true" ]]; then dockerComposeDownAllApps rooted; @@ -97,9 +112,93 @@ dockerSwitcherSwap() reconcileDockerOwnership "$CFG_DOCKER_INSTALL_TYPE"; dockerStartAllApps; databaseOptionInsert "docker_type" $CFG_DOCKER_INSTALL_TYPE; + switchMigrateRestoreApps; fi fi elif [[ $flag == "cli" ]]; then isSuccessful "Docker type is already setup for "$CFG_DOCKER_INSTALL_TYPE" no changes needed..." fi } + +# Apps captured by switchMigrateBackupApps, consumed by switchMigrateRestoreApps. +# Both run inside the same dockerSwitcherSwap call, on either side of the daemon +# switch. +switch_migrated_apps=() + +# Back up every installed app (except LibrePortal's own control plane, which the +# reconcile already carries) under the OLD, still-running mode so it can be +# restored under the new mode afterwards. backupAppStart talks to the daemon via +# CFG_DOCKER_INSTALL_TYPE, which the switcher has already advanced to the target +# mode, so force it (and the resolved install user) back to old_mode for the +# duration, then hand it back. Returns non-zero if any backup fails so the +# caller can abort the switch with nothing changed. +switchMigrateBackupApps() +{ + local old_mode="$1" + + switch_migrated_apps=() + + if [[ -z "$(resticEnabledLocations)" ]]; then + isNotice "No backup location enabled — skipping automatic data migration." + return 0 + fi + + local subdirectories=($(find "$containers_dir" -mindepth 1 -maxdepth 1 -type d)) + + local saved_type="$CFG_DOCKER_INSTALL_TYPE" + CFG_DOCKER_INSTALL_TYPE="$old_mode" + resolveDockerInstallUser + + local failed=() + local dir app_name + for dir in "${subdirectories[@]}"; do + app_name=$(basename "$dir") + [[ "$app_name" == "libreportal" ]] && continue + if backupAppStart "$app_name"; then + switch_migrated_apps+=("$app_name") + else + failed+=("$app_name") + fi + done + + CFG_DOCKER_INSTALL_TYPE="$saved_type" + resolveDockerInstallUser + + if [[ ${#failed[@]} -gt 0 ]]; then + isError "Pre-switch backup failed for: ${failed[*]}" + isNotice "Aborting the switch — nothing changed, apps remain on $old_mode." + return 1 + fi + + isSuccessful "Backed up ${#switch_migrated_apps[@]} app(s) before switching." + return 0 +} + +# Restore each app captured by switchMigrateBackupApps under the NEW mode (new +# daemon up, CFG_DOCKER_INSTALL_TYPE already the target). restoreAppStart wipes +# and re-lays each tree and re-owns it to the new mode's install user — the UID +# remap a chown can't do. Best-effort: a failed app is reported and the rest +# continue, since the daemon has already switched. +switchMigrateRestoreApps() +{ + [[ ${#switch_migrated_apps[@]} -eq 0 ]] && return 0 + + resolveDockerInstallUser + + local failed=() + local app_name + for app_name in "${switch_migrated_apps[@]}"; do + if ! restoreAppStart "$app_name" "latest"; then + failed+=("$app_name") + fi + done + + if [[ ${#failed[@]} -gt 0 ]]; then + isError "Post-switch restore failed for: ${failed[*]}" + isNotice "The switch finished, but these apps need a manual restore: ${failed[*]}" + return 1 + fi + + isSuccessful "Restored ${#switch_migrated_apps[@]} app(s) into $CFG_DOCKER_INSTALL_TYPE." + return 0 +}