98795e31dd
The proxy was using instance.port which is a dynamically allocated host port (e.g., 10001). But containers communicate on the Docker network using their internal ports (8443 for code-server, 8888 for jupyter). This caused connection failures when opening instances. - Add default_port field to ToolType model (null for terminal-only tools) - Create migration 0010 for default_port column - Update seed data: code-server=8443, jupyter=8888, opencode=null - Update proxy to use tool type's default_port instead of instance.port - Update frontend ToolType interface to include default_port Fixes: Opening instances now routes to correct internal container port
231 lines
7.5 KiB
Python
231 lines
7.5 KiB
Python
import logging
|
|
import os
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from sqlalchemy import select, text
|
|
|
|
from src.api.auth import router as auth_router
|
|
from src.api.dashboard import router as dashboard_router
|
|
from src.api.git_repositories import router as git_repositories_router
|
|
from src.api.health import router as health_router
|
|
from src.api.projects import router as projects_router
|
|
from src.api.ssh_keys import router as ssh_keys_router
|
|
from src.api.terminal import router as terminal_router
|
|
from src.api.instance_proxy import router as instance_proxy_router
|
|
from src.api.tool_configs import router as tool_configs_router
|
|
from src.api.tool_instances import router as tool_instances_router
|
|
from src.api.tool_instances import sessions_router
|
|
from src.api.tool_types import router as tool_types_router
|
|
from src.api.user_config import router as user_config_router
|
|
from src.api.users import router as users_router
|
|
from src.config import Settings
|
|
from src.database import SessionLocal, init_database
|
|
from src.logging_config import (
|
|
ExceptionLoggingMiddleware,
|
|
RequestLoggingMiddleware,
|
|
configure_logging,
|
|
)
|
|
from src.models.tool_type import ToolType
|
|
|
|
# Configure logging early
|
|
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
configure_logging(level=getattr(logging, log_level, logging.INFO))
|
|
|
|
logger = logging.getLogger(__name__)
|
|
settings = Settings()
|
|
app = FastAPI(title="Headquarter API")
|
|
|
|
# Configure CORS - must be before other middleware
|
|
# Build allowed origins list including web and api domains
|
|
cors_origins = [settings.web_base_url]
|
|
if settings.api_base_url != settings.web_base_url:
|
|
cors_origins.append(settings.api_base_url)
|
|
logger.info("CORS configured with origins: %s", cors_origins)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.add_middleware(RequestLoggingMiddleware)
|
|
app.add_middleware(ExceptionLoggingMiddleware)
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
"""Log validation errors and return detailed response."""
|
|
errors = exc.errors()
|
|
logger.warning(
|
|
"Validation error for %s %s: %s",
|
|
request.method,
|
|
request.url.path,
|
|
errors,
|
|
)
|
|
return JSONResponse(
|
|
status_code=422,
|
|
content={"detail": errors},
|
|
)
|
|
|
|
|
|
async def _table_exists(session, table_name: str) -> bool:
|
|
"""Check if a table exists in the database."""
|
|
try:
|
|
result = await session.execute(
|
|
text("""
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = :table_name
|
|
)
|
|
"""),
|
|
{"table_name": table_name},
|
|
)
|
|
return result.scalar() or False
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def seed_builtin_tool_types():
|
|
async with SessionLocal() as session:
|
|
# Check if tool_types table exists before attempting to seed
|
|
if not await _table_exists(session, "tool_types"):
|
|
logger.warning(
|
|
"tool_types table does not exist. Skipping seeding. "
|
|
"Migrations may not have run yet."
|
|
)
|
|
return
|
|
|
|
builtin_types = [
|
|
{
|
|
"name": "code-server",
|
|
"display_name": "VS Code Server",
|
|
"description": "VS Code running in the browser via code-server",
|
|
"category": "editor",
|
|
"interfaces": ["web"],
|
|
"compose_template": """version: "3.8"
|
|
services:
|
|
code-server:
|
|
image: lscr.io/linuxserver/code-server:latest
|
|
container_name: {{TOOL_NAME}}
|
|
environment:
|
|
- PUID=1000
|
|
- PGID=1000
|
|
- TZ=Europe/London
|
|
volumes:
|
|
- {{REPO_PATH}}:/config/workspace
|
|
ports:
|
|
- "8443:8443"
|
|
restart: unless-stopped""",
|
|
"default_port": 8443,
|
|
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
|
},
|
|
{
|
|
"name": "jupyter-notebook",
|
|
"display_name": "Jupyter Notebook",
|
|
"description": "Jupyter Lab for interactive development",
|
|
"category": "notebook",
|
|
"interfaces": ["web"],
|
|
"default_port": 8888,
|
|
"compose_template": """version: "3.8"
|
|
services:
|
|
jupyter:
|
|
image: jupyter/scipy-notebook:latest
|
|
container_name: {{TOOL_NAME}}
|
|
environment:
|
|
- JUPYTER_ENABLE_LAB=yes
|
|
volumes:
|
|
- {{REPO_PATH}}:/home/jovyan/work
|
|
ports:
|
|
- "8888:8888"
|
|
restart: unless-stopped""",
|
|
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
|
},
|
|
{
|
|
"name": "opencode",
|
|
"display_name": "OpenCode",
|
|
"description": "AI coding assistant in the terminal",
|
|
"category": "ai-assistant",
|
|
"interfaces": ["terminal"],
|
|
"compose_template": """version: "3.8"
|
|
services:
|
|
opencode:
|
|
image: node:20-slim
|
|
container_name: {{TOOL_NAME}}
|
|
working_dir: /workspace
|
|
environment:
|
|
- HOME=/tmp
|
|
volumes:
|
|
- {{REPO_PATH}}:/workspace
|
|
- opencode_home:/tmp
|
|
command: >
|
|
sh -c "npm install -g opencode@latest &&
|
|
mkdir -p /workspace &&
|
|
tail -f /dev/null"
|
|
stdin_open: true
|
|
tty: true
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
opencode_home:""",
|
|
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
|
},
|
|
]
|
|
|
|
for tool_data in builtin_types:
|
|
existing = await session.scalar(select(ToolType).where(ToolType.name == tool_data["name"]))
|
|
if not existing:
|
|
tool_type = ToolType(
|
|
name=tool_data["name"],
|
|
display_name=tool_data["display_name"],
|
|
description=tool_data["description"],
|
|
category=tool_data["category"],
|
|
interfaces=tool_data["interfaces"],
|
|
compose_template=tool_data["compose_template"],
|
|
required_variables=tool_data["required_variables"],
|
|
default_port=tool_data.get("default_port"),
|
|
is_builtin=True,
|
|
)
|
|
session.add(tool_type)
|
|
|
|
await session.commit()
|
|
logger.info("Built-in tool types seeded successfully.")
|
|
|
|
|
|
@app.on_event("startup")
|
|
async def on_startup():
|
|
logger.info("Starting up Headquarter API...")
|
|
|
|
# Initialize database (run migrations)
|
|
db_ready = await init_database()
|
|
if not db_ready:
|
|
logger.error("Database initialization failed. Shutting down.")
|
|
import sys
|
|
sys.exit(1)
|
|
|
|
# Seed built-in data
|
|
await seed_builtin_tool_types()
|
|
logger.info("Startup complete.")
|
|
|
|
app.include_router(health_router)
|
|
app.include_router(auth_router)
|
|
app.include_router(dashboard_router)
|
|
app.include_router(projects_router)
|
|
app.include_router(users_router)
|
|
app.include_router(ssh_keys_router)
|
|
app.include_router(git_repositories_router)
|
|
app.include_router(user_config_router)
|
|
app.include_router(tool_types_router)
|
|
app.include_router(tool_instances_router)
|
|
app.include_router(tool_configs_router)
|
|
app.include_router(sessions_router)
|
|
app.include_router(instance_proxy_router)
|
|
app.include_router(terminal_router)
|
|
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|