stronghold/backend/main.py
robojerk c31e48e2b1 Proof of concept: Container management with FastAPI backend
- FastAPI backend with REST API endpoints
- SQLite database for container metadata
- Docker/Podman SDK integration with label filtering
- Frontend: Server creation form and management page
- Container operations: create, list, start, stop, delete
- Single container deployment (nginx + Python + supervisor)
- Support for Docker and Podman (rootless)
- Volume management for persistent data
2025-10-31 11:50:31 -07:00

195 lines
6.2 KiB
Python

from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import List, Optional
import json
from backend.database import get_db, Container, Base, engine
from backend.docker_manager import (
get_docker_client, list_containers, create_container,
start_container, stop_container, remove_container
)
# Create tables
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Stronghold API")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Mount static files (frontend will be served by nginx in production)
# app.mount("/", StaticFiles(directory="../frontend", html=True), name="static")
# Pydantic models
class ContainerConfig(BaseModel):
serverName: str
serverType: str
version: Optional[str] = "LATEST"
memory: Optional[str] = "2G"
port: Optional[str] = "25565"
difficulty: Optional[str] = None
gamemode: Optional[str] = None
levelType: Optional[str] = None
motd: Optional[str] = None
maxPlayers: Optional[int] = 20
viewDistance: Optional[int] = 10
acceptEULA: bool = True
enableRCON: bool = False
rconPort: Optional[str] = "25575"
rconPassword: Optional[str] = None
pvpEnabled: bool = True
allowFlight: bool = False
restartPolicy: str = "unless-stopped"
class ContainerResponse(BaseModel):
id: int
container_id: str
name: str
image: str
status: str
created_at: str
# API Routes
@app.get("/api/containers")
def get_containers(db: Session = Depends(get_db)):
"""List all Stronghold-managed containers"""
try:
client = get_docker_client()
docker_containers = list_containers(client)
# Get containers from database
db_containers = db.query(Container).all()
# Merge data
result = []
for db_container in db_containers:
# Find matching docker container
docker_container = next(
(dc for dc in docker_containers if dc.id == db_container.container_id),
None
)
status = docker_container.status if docker_container else "unknown"
result.append({
"id": db_container.id,
"container_id": db_container.container_id,
"name": db_container.name,
"image": db_container.image,
"status": status,
"created_at": db_container.created_at.isoformat() if db_container.created_at else None
})
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/containers")
def create_container_endpoint(config: ContainerConfig, db: Session = Depends(get_db)):
"""Create a new Minecraft server container"""
try:
client = get_docker_client()
# Check if name already exists
existing = db.query(Container).filter(Container.name == config.serverName).first()
if existing:
raise HTTPException(status_code=400, detail="Container name already exists")
# Create container
docker_container = create_container(client, config.serverName, config.dict())
# Save to database
db_container = Container(
container_id=docker_container.id,
name=config.serverName,
image="itzg/minecraft-server",
status="created",
config_json=json.dumps(config.dict())
)
db.add(db_container)
db.commit()
db.refresh(db_container)
# Start container
docker_container.start()
# Update status
db_container.status = "running"
db.commit()
return {
"id": db_container.id,
"container_id": docker_container.id,
"name": config.serverName,
"status": "running"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/containers/{container_id}/start")
def start_container_endpoint(container_id: str, db: Session = Depends(get_db)):
"""Start a container"""
try:
client = get_docker_client()
container = start_container(client, container_id)
# Update database
db_container = db.query(Container).filter(Container.container_id == container_id).first()
if db_container:
db_container.status = "running"
db.commit()
return {"status": "started", "container_id": container_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/containers/{container_id}/stop")
def stop_container_endpoint(container_id: str, db: Session = Depends(get_db)):
"""Stop a container"""
try:
client = get_docker_client()
container = stop_container(client, container_id)
# Update database
db_container = db.query(Container).filter(Container.container_id == container_id).first()
if db_container:
db_container.status = "stopped"
db.commit()
return {"status": "stopped", "container_id": container_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/containers/{container_id}")
def delete_container_endpoint(container_id: str, db: Session = Depends(get_db)):
"""Delete a container"""
try:
client = get_docker_client()
remove_container(client, container_id)
# Remove from database
db_container = db.query(Container).filter(Container.container_id == container_id).first()
if db_container:
db.delete(db_container)
db.commit()
return {"status": "deleted", "container_id": container_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/health")
def health_check():
"""Health check endpoint"""
return {"status": "ok"}