Initial Commit

This commit is contained in:
Alphaeus Mote
2025-11-19 08:09:34 -05:00
parent 3da6627c2d
commit be6d358086
78 changed files with 13135 additions and 1 deletions
+194
View File
@@ -0,0 +1,194 @@
"""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)}"
)