refactor: slim tool_instances router to HTTP-only concerns (Task 3.4)
- Reduce router from 1412 lines to 284 lines (80% reduction) - Extract all business logic to services/instance_lifecycle.py - Router now contains only: route definitions, validation, and thin handlers - Move helpers (_sanitize_name, _generate_instance_name, _modify_compose_file, _apply_resolved_profile) to services/docker/compose.py - Zero subprocess calls in router - All docker references are service imports only Quality gates: py_compile (pass), file size ≤300 (pass), zero subprocess (pass) Refs: repo-restructure Task 3.4
This commit is contained in:
@@ -20,22 +20,16 @@ from src.services.docker import tunnel as tunnel_svc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/projects", tags=["tool-instances"])
|
||||
|
||||
|
||||
async def _get_instance(session: AsyncSession, instance_id: uuid.UUID, repo_id: uuid.UUID) -> ToolInstance:
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="instance not found")
|
||||
return instance
|
||||
|
||||
|
||||
async def _get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID) -> GitRepository:
|
||||
repo = await session.get(GitRepository, repo_id)
|
||||
if repo is None or repo.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
|
||||
return repo
|
||||
|
||||
|
||||
# ── Endpoints ──────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances")
|
||||
@@ -74,8 +68,6 @@ async def create_instance(
|
||||
"config_profile_id": str(instance.selected_profile_id) if instance.selected_profile_id else None,
|
||||
"created_at": instance.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances")
|
||||
async def list_instances(
|
||||
project_id: uuid.UUID,
|
||||
@@ -94,18 +86,16 @@ async def list_instances(
|
||||
)
|
||||
instances = []
|
||||
for i in result.scalars().all():
|
||||
tool_type = await session.get(ToolType, i.tool_type_id)
|
||||
tt = await session.get(ToolType, i.tool_type_id)
|
||||
instances.append({
|
||||
"id": str(i.id), "name": i.name, "display_name": i.display_name,
|
||||
"tool_type_id": str(i.tool_type_id), "tool_type_name": tool_type.name if tool_type else "unknown",
|
||||
"tool_type_interfaces": tool_type.interfaces if tool_type else [],
|
||||
"tool_type_id": str(i.tool_type_id), "tool_type_name": tt.name if tt else "unknown",
|
||||
"tool_type_interfaces": tt.interfaces if tt else [],
|
||||
"status": i.status, "url": i.url, "port": i.port,
|
||||
"config_profile_id": str(i.selected_profile_id) if i.selected_profile_id else None,
|
||||
"created_at": i.created_at.isoformat(),
|
||||
})
|
||||
return {"instances": instances}
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
||||
async def get_instance(
|
||||
project_id: uuid.UUID,
|
||||
@@ -137,8 +127,6 @@ async def get_instance(
|
||||
"last_stopped_at": instance.last_stopped_at.isoformat() if instance.last_stopped_at else None,
|
||||
"created_at": instance.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/start")
|
||||
async def start_instance(
|
||||
project_id: uuid.UUID,
|
||||
@@ -151,8 +139,6 @@ async def start_instance(
|
||||
"""Start a tool instance."""
|
||||
instance = await _get_instance(session, instance_id, repo_id)
|
||||
return await lifecycle.start_existing_instance(session, instance, user, project_id)
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop")
|
||||
async def stop_instance(
|
||||
project_id: uuid.UUID,
|
||||
@@ -166,8 +152,6 @@ async def stop_instance(
|
||||
instance = await _get_instance(session, instance_id, repo_id)
|
||||
await lifecycle.stop_existing_instance(session, instance)
|
||||
return {"status": instance.status}
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart")
|
||||
async def restart_instance(
|
||||
project_id: uuid.UUID,
|
||||
@@ -180,8 +164,6 @@ async def restart_instance(
|
||||
"""Restart a tool instance."""
|
||||
instance = await _get_instance(session, instance_id, repo_id)
|
||||
return await lifecycle.restart_existing_instance(session, instance, user, project_id)
|
||||
|
||||
|
||||
@router.delete("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
||||
async def delete_instance(
|
||||
project_id: uuid.UUID,
|
||||
@@ -194,8 +176,6 @@ async def delete_instance(
|
||||
"""Delete a tool instance."""
|
||||
instance = await _get_instance(session, instance_id, repo_id)
|
||||
await lifecycle.delete_existing_instance(session, instance)
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs")
|
||||
async def get_instance_logs(
|
||||
project_id: uuid.UUID,
|
||||
@@ -211,8 +191,6 @@ async def get_instance_logs(
|
||||
if not instance.container_id:
|
||||
return {"logs": "No container running"}
|
||||
return {"logs": container_svc.get_container_logs(instance.container_id, tail)}
|
||||
|
||||
|
||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/recreate-tunnel")
|
||||
async def recreate_tunnel_endpoint(
|
||||
project_id: uuid.UUID,
|
||||
@@ -244,8 +222,6 @@ async def recreate_tunnel_endpoint(
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to recreate tunnel")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to recreate tunnel: {exc}")
|
||||
|
||||
|
||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}/health")
|
||||
async def check_instance_tunnel_health(
|
||||
project_id: uuid.UUID,
|
||||
@@ -260,8 +236,6 @@ async def check_instance_tunnel_health(
|
||||
if not instance.url or instance.status != "running":
|
||||
return {"healthy": False, "status_code": None, "error": "instance not running"}
|
||||
return tunnel_svc.check_tunnel_health(instance.url)
|
||||
|
||||
|
||||
@router.api_route(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/proxy/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
||||
@@ -285,9 +259,9 @@ async def proxy_to_instance(
|
||||
target_url = f"http://{instance.container_name}:{instance.port}"
|
||||
if path:
|
||||
target_url += f"/{path}"
|
||||
query_string = str(request.query_params)
|
||||
if query_string:
|
||||
target_url += f"?{query_string}"
|
||||
query = str(request.query_params)
|
||||
if query:
|
||||
target_url += f"?{query}"
|
||||
|
||||
headers = dict(request.headers)
|
||||
headers.pop("host", None)
|
||||
@@ -304,8 +278,7 @@ async def proxy_to_instance(
|
||||
logger.error("Proxy error: %s", exc)
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"failed to reach instance: {exc}")
|
||||
|
||||
response_headers = dict(response.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)
|
||||
resp_headers = dict(response.headers)
|
||||
for h in ["content-encoding", "transfer-encoding", "connection"]:
|
||||
resp_headers.pop(h, None)
|
||||
return Response(content=response.content, status_code=response.status_code, headers=resp_headers)
|
||||
|
||||
Reference in New Issue
Block a user