"""System update API endpoints""" from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Optional from app.api.auth import get_current_user from app.core.config import settings import logging import subprocess import os import requests logger = logging.getLogger(__name__) router = APIRouter() # Update server configuration UPDATE_SERVER = "http://deploy.agit8or.net" UPDATE_ENDPOINT = f"{UPDATE_SERVER}/api/v1/system-updates/version" class UpdateCheckResponse(BaseModel): current_version: str latest_version: str update_available: bool download_url: Optional[str] = None release_notes: Optional[str] = None @router.get("/check") def check_for_updates(current_user=Depends(get_current_user)): """Check if updates are available from the main server""" try: # Get current version current_version = settings.APP_VERSION # Query update server for latest version try: response = requests.get(UPDATE_ENDPOINT, timeout=10) if response.status_code == 200: update_info = response.json() latest_version = update_info.get("version", current_version) # Simple version comparison (assumes semantic versioning) update_available = latest_version != current_version return UpdateCheckResponse( current_version=current_version, latest_version=latest_version, update_available=update_available, download_url=update_info.get("download_url") if update_available else None, release_notes=update_info.get("release_notes") ) else: raise Exception(f"Update server returned {response.status_code}") except requests.RequestException as e: logger.warning(f"Could not reach update server: {e}") return UpdateCheckResponse( current_version=current_version, latest_version=current_version, update_available=False, release_notes="Could not reach update server" ) except Exception as e: logger.error(f"Failed to check for updates: {e}") raise HTTPException( status_code=500, detail=f"Failed to check for updates: {str(e)}" ) @router.get("/version") def get_version_info(): """ Serve version information for update clients This endpoint is called BY OTHER INSTANCES to check for updates """ return { "version": settings.APP_VERSION, "download_url": f"{UPDATE_SERVER}/api/v1/system-updates/download", "install_url": f"{UPDATE_SERVER}/install.sh", "release_notes": f""" Depl0y {settings.APP_VERSION} Release Notes: ✨ New in v1.1.9: - Automatic backend restart after upgrades - Updates now apply immediately without manual intervention - Backend automatically loads new code after update completes ✨ New in v1.1.8: - Fixed JavaScript error preventing Settings page from loading - Fixed installer version mismatch - Updates work correctly in one step ✨ Previous versions: - One-click automatic updates from Settings (v1.1.5) - Fast 30-second upgrades with pre-built frontend (v1.1.5) - System Updates section with loading/error states (v1.1.7) - Automated cloud image deployment - Inter-node SSH setup for clusters """.strip() } @router.get("/download") def download_update(): """Download the latest update package (public endpoint for automated updates)""" try: # Use pre-packaged file package_path = "/opt/depl0y/depl0y-v1.1.5.tar.gz" if not os.path.exists(package_path): # Fallback: create package on-the-fly temp_package = "/tmp/depl0y-update.tar.gz" create_package_cmd = f""" cd /home/administrator/depl0y && \ tar -czf {temp_package} \ --exclude='node_modules' \ --exclude='dist' \ --exclude='.git' \ --exclude='__pycache__' \ --exclude='*.pyc' \ --exclude='venv' \ backend/ frontend/ *.md *.sh 2>/dev/null || true """ subprocess.run(create_package_cmd, shell=True, check=True) package_path = temp_package if not os.path.exists(package_path): raise HTTPException(status_code=500, detail="Failed to find or create update package") return FileResponse( package_path, media_type="application/gzip", filename="depl0y-latest.tar.gz", headers={ "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0" } ) except Exception as e: logger.error(f"Failed to serve update package: {e}") raise HTTPException( status_code=500, detail=f"Failed to serve update package: {str(e)}" ) @router.post("/apply") def apply_update(current_user=Depends(get_current_user)): """Apply update by downloading and running the installer script""" try: logger.info("Starting update process via installer...") # Download installer script installer_url = f"{UPDATE_SERVER}/downloads/install.sh" installer_path = "/tmp/depl0y-update-install.sh" logger.info(f"Downloading installer from {installer_url}") response = requests.get(installer_url, timeout=30) if response.status_code != 200: raise Exception(f"Failed to download installer: HTTP {response.status_code}") # Save installer with open(installer_path, 'wb') as f: f.write(response.content) # Make executable os.chmod(installer_path, 0o755) logger.info("Installer downloaded, starting update in background...") # Run installer in background (it will detect existing installation and upgrade) update_script = f"/usr/bin/sudo /opt/depl0y/scripts/update-wrapper.sh {installer_path}" subprocess.Popen(update_script, shell=True) return { "success": True, "message": "Update is being applied. The installer will upgrade your installation while preserving your data. The service will restart automatically.", "log_file": "/tmp/depl0y-update.log" } except Exception as e: logger.error(f"Failed to start update: {e}") raise HTTPException( status_code=500, detail=f"Failed to start update: {str(e)}" )