37ccaa4fdc
Service organization (19 files moved into 6 subpackages): - services/instance/ — event_bus, health_monitor, lifecycle_hooks - services/config/ — config_profile_resolver - services/git/ — clone, git_operations, git_service - services/build/ — docker_build, manifest_compiler - services/terminal/ — terminal_manager, terminal_session - services/shared/ — correlation, file_service, notification_service, permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager API router organization (16 files moved into 6 subpackages): - api/tool/ — tool_instances, tool_types, tool_definitions, tool_types_validation, sessions (extracted from tool_instances) - api/config/ — config_profiles, user_config - api/workspace/ — workspaces, workspace_files, workspace_git, workspace_instances - api/user/ — users, auth, ssh_keys - api/project/ — projects, git_repositories - api/system/ — health, events, notifications, dashboard, terminal, instance_proxy Updated main.py imports and all __init__.py re-exports. Sessions router extracted from tool_instances.py into api/tool/sessions.py. Quality gates: py_compile passed, ruff passed.
125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""Instance proxy router for forwarding HTTP requests to running containers."""
|
|
|
|
import logging
|
|
import uuid
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.auth.dependencies import get_current_user_id, get_db_session
|
|
from src.models import ToolInstance
|
|
from src.models import ToolType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/instances", tags=["instance-proxy"])
|
|
|
|
|
|
async def _proxy_request(
|
|
request: Request,
|
|
instance_id: uuid.UUID,
|
|
path: str,
|
|
user_id: uuid.UUID,
|
|
session: AsyncSession,
|
|
) -> Response:
|
|
"""Proxy an HTTP request to a running instance."""
|
|
instance = await session.get(ToolInstance, instance_id)
|
|
if instance is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="instance not found"
|
|
)
|
|
|
|
# Verify ownership
|
|
if instance.owner_id != user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="not authorized to access this instance",
|
|
)
|
|
|
|
if instance.status != "running" or not instance.container_name:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="instance is not running",
|
|
)
|
|
|
|
# Get the tool type to find the internal port
|
|
tool_type = await session.get(ToolType, instance.tool_type_id)
|
|
internal_port = tool_type.default_port if tool_type and tool_type.default_port else instance.port
|
|
|
|
# Build target URL using internal port
|
|
target_url = f"http://{instance.container_name}:{internal_port}"
|
|
if path:
|
|
target_url += f"/{path}"
|
|
|
|
# Get query string
|
|
query_string = str(request.query_params)
|
|
if query_string:
|
|
target_url += f"?{query_string}"
|
|
|
|
# Forward headers (excluding host and cookies)
|
|
headers: dict[str, str] = {}
|
|
for key, value in request.headers.items():
|
|
if key.lower() not in ("host", "cookie", "content-length"):
|
|
headers[key] = value
|
|
|
|
# Forward the request
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
body = await request.body()
|
|
response = await client.request(
|
|
method=request.method,
|
|
url=target_url,
|
|
headers=headers,
|
|
content=body,
|
|
follow_redirects=False,
|
|
timeout=30.0,
|
|
)
|
|
except Exception as exc:
|
|
logger.error("Proxy error to %s: %s", target_url, exc)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"failed to reach instance: {exc}",
|
|
)
|
|
|
|
# Build response
|
|
response_headers = dict(response.headers)
|
|
# Remove hop-by-hop headers
|
|
for header in ("content-encoding", "transfer-encoding", "connection"):
|
|
response_headers.pop(header, None)
|
|
|
|
return Response(
|
|
content=response.content,
|
|
status_code=response.status_code,
|
|
headers=response_headers,
|
|
)
|
|
|
|
|
|
@router.get("/{instance_id}/proxy/{path:path}")
|
|
@router.post("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.put("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.delete("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.patch("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.head("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
@router.options("/{instance_id}/proxy/{path:path}", include_in_schema=False)
|
|
async def proxy_to_instance(
|
|
request: Request,
|
|
instance_id: uuid.UUID,
|
|
path: str = "",
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Response:
|
|
"""Proxy requests to a running tool instance.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
instance_id: UUID of the instance.
|
|
path: The path to proxy to the instance.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
Response from the proxied instance.
|
|
"""
|
|
return await _proxy_request(request, instance_id, path, user_id, session)
|