From a6fb9e5e1844553e9868374c572025c4907c6a96 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Aug 2026 11:30:14 +0200 Subject: [PATCH] Wire management-platform's backup/restore routes to the k8s scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api_backup_run and restore_start were still hardcoded to the Docker-era backup-myapps.sh/restore-myapps.sh, which target Docker volumes that no longer exist for the 5 apps now running in k3s (n8n, odoo, mautic, nextcloud, frappe/erpnext) — every backup/restore triggered from the UI has been silently hollow for these apps since the migration. - api_backup_run: /root/backup-myapps.sh -> /root/CloudOps/backup/backup-k8s-apps.sh - restore_start: switched from the old image-relative path (which resolved to /app/restore-myapps.sh, baked into the Docker image from platform/) to an absolute /root/CloudOps/backup/restore-k8s-apps.sh path — the new backup/ folder is a sibling of platform/, not part of the Docker build context, so it's only reachable via the pod's existing /root hostPath mount, the same way api_backup_run already reaches its script. --apps flag shape and the job-log streaming contract are unchanged, so no frontend/template changes needed for the core flow. modules/backups.py's three hardcoded myapps-backup-* references (get_local_backups, get_vm_backups x2, delete_backup's filename regex) now also recognize the new myapps-k8s-backup-* prefix, so both backup lineages stay visible/manageable in the UI. Depends on the previous commit (scoped ServiceAccount + k3s kubectl access for the pod) to actually function when triggered from the UI. Co-Authored-By: Claude Sonnet 5 --- platform/app.py | 23 +++++++++++++---------- platform/modules/backups.py | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/platform/app.py b/platform/app.py index 8aca4c6..87c2d2c 100644 --- a/platform/app.py +++ b/platform/app.py @@ -424,7 +424,7 @@ def api_backup_run(): 'message': 'Manual backup can only be triggered from the main server platform.' }), 400 - script = '/root/backup-myapps.sh' + script = '/root/CloudOps/backup/backup-k8s-apps.sh' if not os.path.exists(script): return jsonify({ 'success': False, @@ -818,11 +818,14 @@ def restore_start(): if not os.path.exists(backup_path): return jsonify({'error': f'Not found: {backup_path}'}), 400 - restore_script_local = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'restore-myapps.sh' - ) + # Absolute /root path (not image-relative like the old restore-myapps.sh + # was) — backup/restore-k8s-apps.sh lives in the CloudOps repo's backup/ + # folder, a sibling of platform/, so it's never baked into the Docker + # image; it's reached the same way api_backup_run's script is, via the + # pod's existing rw hostPath mount of /root. + restore_script_local = '/root/CloudOps/backup/restore-k8s-apps.sh' if not os.path.exists(restore_script_local): - return jsonify({'error': f'restore-myapps.sh not found at {restore_script_local}'}), 500 + return jsonify({'error': f'restore-k8s-apps.sh not found at {restore_script_local}'}), 500 if target == 'local': hostname = os.uname().nodename @@ -835,9 +838,9 @@ def restore_start(): f"mkdir -p {session_dir} && " f"echo 'Extracting backup...' && " f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && " - f"cp {restore_script_local} {session_dir}/restore-myapps.sh && " - f"chmod +x {session_dir}/restore-myapps.sh && " - f"cd {session_dir} && bash restore-myapps.sh{apps_flag} ; " + f"cp {restore_script_local} {session_dir}/restore-k8s-apps.sh && " + f"chmod +x {session_dir}/restore-k8s-apps.sh && " + f"cd {session_dir} && bash restore-k8s-apps.sh{apps_flag} ; " f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT" ) else: @@ -865,12 +868,12 @@ def restore_start(): f"echo 'Copying backup archive...' && " f"{scp_prefix} {backup_path} {remote_user}@{remote_ip}:{remote_dest}/{backup_file} && " f"echo 'Copying restore script...' && " - f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-myapps.sh && " + f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-k8s-apps.sh && " f"echo 'Running restore on {remote_ip}:{remote_port}...' && " f"{ssh_prefix} {remote_user}@{remote_ip} " f"'set -e && cd {remote_dest} && " f"tar -xzf {backup_file} --strip-components=1 && " - f"chmod +x restore-myapps.sh && bash restore-myapps.sh{apps_flag}' ; " + f"chmod +x restore-k8s-apps.sh && bash restore-k8s-apps.sh{apps_flag}' ; " f"EXIT=$? ; " f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; " f"exit $EXIT" diff --git a/platform/modules/backups.py b/platform/modules/backups.py index ab2905c..9be1c73 100644 --- a/platform/modules/backups.py +++ b/platform/modules/backups.py @@ -56,8 +56,12 @@ def _ssh_main(remote_cmd, timeout=30): # ──────────────────────────────────────────────────────────────── def get_local_backups(): + # myapps-backup-* is the legacy Docker-era lineage; myapps-k8s-backup-* + # is the k8s-native one (backup-k8s-apps.sh). Listed together, sorted + # by mtime, so history from both stays visible in the UI. stdout, _ = _ssh_main( - "ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null | head -20" + "ls -t /root/backups/myapps-backup-*.tar.gz " + "/root/backups/myapps-k8s-backup-*.tar.gz 2>/dev/null | head -20" ) files = [] if stdout: @@ -77,7 +81,8 @@ def get_vm_backups(): f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " f"-o BatchMode=yes " f"{VM_USER}@{VM_HOST} " - f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz 2>/dev/null | head -20'" + f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz " + f"/backups/cloudproject/myapps-k8s-backup-*.tar.gz 2>/dev/null | head -20'" ) stdout, _ = _run(cmd, timeout=25) if stdout: @@ -90,7 +95,10 @@ def get_vm_backups(): else: backup_dir = '/backups/cloudproject' if os.path.exists(backup_dir): - files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz') + files = ( + glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz') + + glob.glob(f'{backup_dir}/myapps-k8s-backup-*.tar.gz') + ) files.sort(key=os.path.getmtime, reverse=True) vm_backups = [os.path.basename(f) for f in files[:20]] return vm_backups @@ -408,7 +416,11 @@ def _sha256_file(path): # ──────────────────────────────────────────────────────────────── def delete_backup(backup_file, source='local'): - if not re.match(r'^myapps-backup-\d{8}_\d{6}\.tar\.gz$', backup_file): + # Accepts both the legacy Docker-era lineage (myapps-backup-*) and the + # k8s-native one (myapps-k8s-backup-*, from backup-k8s-apps.sh). Still a + # tight anchor match — this guards against path traversal in a filename + # that ends up interpolated into a shell/filesystem path below. + if not re.match(r'^myapps-(k8s-)?backup-\d{8}_\d{6}\.tar\.gz$', backup_file): return False, f'Invalid backup filename: {backup_file}' if source == 'local':