"""StackPilot backend — FastAPI application entry point.""" from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from sqlmodel import Session from config import settings from version import APP_VERSION from database import engine, init_db from docker_client import DockerError from routers import ( agents, audit, auth, backups, containers, dashboard, destinations, editor, files, images, networks, ports, schedules, secrets, settings as settings_router, stacks, system, templates, volumes, ws, ) from services import ( backup_destination_service, image_status_store, schedule_service, stack_lock_service, template_service, update_service, ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("stackpilot") @asynccontextmanager async def lifespan(app: FastAPI): init_db() # Register stacks that already exist on disk. try: with Session(engine) as session: stacks.sync_discovered_stacks(session) except Exception as exc: # noqa: BLE001 logger.warning("Stack discovery failed: %s", exc) # One-off: encrypt backup-destination credentials written before they were # stored encrypted (see services/crypto_service.py). try: with Session(engine) as session: encrypted = backup_destination_service.migrate_plaintext_configs(session) if encrypted: logger.info("Encrypted %d backup destination config(s) at rest", encrypted) except Exception as exc: # noqa: BLE001 logger.warning("Destination config encryption migration failed: %s", exc) try: moved = template_service.migrate_legacy_db_templates() if moved: logger.info("Migrated %d custom template(s) from the database to folders", moved) except Exception as exc: # noqa: BLE001 logger.warning("Legacy template migration failed: %s", exc) # Runtime state that used to live in module dicts and was lost on restart. try: with Session(engine) as session: stale = stack_lock_service.prune_expired(session) if stale: logger.info("Cleared %d stale stack lock(s) from a previous run", stale) except Exception as exc: # noqa: BLE001 logger.warning("Could not prune stack locks: %s", exc) try: restored = image_status_store.install() logger.info("Restored %d cached image update status(es)", restored) except Exception as exc: # noqa: BLE001 logger.warning("Could not restore the image update cache: %s", exc) update_task = asyncio.create_task(update_service.background_loop()) schedule_task = asyncio.create_task(schedule_service.scheduler_loop()) logger.info("StackPilot backend ready on port %s", settings.PORT) yield update_task.cancel() schedule_task.cancel() app = FastAPI(title="StackPilot", version=APP_VERSION, lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(DockerError) async def docker_error_handler(_request: Request, exc: DockerError): return JSONResponse( status_code=502, content={"error": exc.error, "detail": exc.detail}, ) app.include_router(auth.router) app.include_router(stacks.router) app.include_router(secrets.router) app.include_router(containers.router) app.include_router(dashboard.router) app.include_router(system.router) app.include_router(volumes.router) app.include_router(editor.router) app.include_router(files.router) app.include_router(images.router) app.include_router(ports.router) app.include_router(templates.router) app.include_router(audit.router) app.include_router(settings_router.router) app.include_router(backups.router) app.include_router(destinations.router) app.include_router(schedules.router) app.include_router(networks.router) app.include_router(agents.router) app.include_router(ws.router) @app.get("/api/health") def health() -> dict: return {"status": "ok", "version": APP_VERSION}