mirror of
https://github.com/Grace-Solutions/Depl0y-Custom.git
synced 2026-07-26 11:28:19 +00:00
195 lines
6.6 KiB
Python
195 lines
6.6 KiB
Python
"""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)}"
|
|
)
|
|
|
|
|