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':