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__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/projects", tags=["tool-instances"])
|
router = APIRouter(prefix="/projects", tags=["tool-instances"])
|
||||||
|
|
||||||
|
|
||||||
async def _get_instance(session: AsyncSession, instance_id: uuid.UUID, repo_id: uuid.UUID) -> ToolInstance:
|
async def _get_instance(session: AsyncSession, instance_id: uuid.UUID, repo_id: uuid.UUID) -> ToolInstance:
|
||||||
instance = await session.get(ToolInstance, instance_id)
|
instance = await session.get(ToolInstance, instance_id)
|
||||||
if instance is None or instance.repository_id != repo_id:
|
if instance is None or instance.repository_id != repo_id:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="instance not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="instance not found")
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
async def _get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID) -> GitRepository:
|
async def _get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID) -> GitRepository:
|
||||||
repo = await session.get(GitRepository, repo_id)
|
repo = await session.get(GitRepository, repo_id)
|
||||||
if repo is None or repo.project_id != project_id:
|
if repo is None or repo.project_id != project_id:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
|
||||||
return repo
|
return repo
|
||||||
|
|
||||||
|
|
||||||
# ── Endpoints ──────────────────────────────────────────────────────────────
|
# ── Endpoints ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.post("/{project_id}/repositories/{repo_id}/instances")
|
@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,
|
"config_profile_id": str(instance.selected_profile_id) if instance.selected_profile_id else None,
|
||||||
"created_at": instance.created_at.isoformat(),
|
"created_at": instance.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{project_id}/repositories/{repo_id}/instances")
|
@router.get("/{project_id}/repositories/{repo_id}/instances")
|
||||||
async def list_instances(
|
async def list_instances(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -94,18 +86,16 @@ async def list_instances(
|
|||||||
)
|
)
|
||||||
instances = []
|
instances = []
|
||||||
for i in result.scalars().all():
|
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({
|
instances.append({
|
||||||
"id": str(i.id), "name": i.name, "display_name": i.display_name,
|
"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_id": str(i.tool_type_id), "tool_type_name": tt.name if tt else "unknown",
|
||||||
"tool_type_interfaces": tool_type.interfaces if tool_type else [],
|
"tool_type_interfaces": tt.interfaces if tt else [],
|
||||||
"status": i.status, "url": i.url, "port": i.port,
|
"status": i.status, "url": i.url, "port": i.port,
|
||||||
"config_profile_id": str(i.selected_profile_id) if i.selected_profile_id else None,
|
"config_profile_id": str(i.selected_profile_id) if i.selected_profile_id else None,
|
||||||
"created_at": i.created_at.isoformat(),
|
"created_at": i.created_at.isoformat(),
|
||||||
})
|
})
|
||||||
return {"instances": instances}
|
return {"instances": instances}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
||||||
async def get_instance(
|
async def get_instance(
|
||||||
project_id: uuid.UUID,
|
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,
|
"last_stopped_at": instance.last_stopped_at.isoformat() if instance.last_stopped_at else None,
|
||||||
"created_at": instance.created_at.isoformat(),
|
"created_at": instance.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/start")
|
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/start")
|
||||||
async def start_instance(
|
async def start_instance(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -151,8 +139,6 @@ async def start_instance(
|
|||||||
"""Start a tool instance."""
|
"""Start a tool instance."""
|
||||||
instance = await _get_instance(session, instance_id, repo_id)
|
instance = await _get_instance(session, instance_id, repo_id)
|
||||||
return await lifecycle.start_existing_instance(session, instance, user, project_id)
|
return await lifecycle.start_existing_instance(session, instance, user, project_id)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop")
|
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop")
|
||||||
async def stop_instance(
|
async def stop_instance(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -166,8 +152,6 @@ async def stop_instance(
|
|||||||
instance = await _get_instance(session, instance_id, repo_id)
|
instance = await _get_instance(session, instance_id, repo_id)
|
||||||
await lifecycle.stop_existing_instance(session, instance)
|
await lifecycle.stop_existing_instance(session, instance)
|
||||||
return {"status": instance.status}
|
return {"status": instance.status}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart")
|
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart")
|
||||||
async def restart_instance(
|
async def restart_instance(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -180,8 +164,6 @@ async def restart_instance(
|
|||||||
"""Restart a tool instance."""
|
"""Restart a tool instance."""
|
||||||
instance = await _get_instance(session, instance_id, repo_id)
|
instance = await _get_instance(session, instance_id, repo_id)
|
||||||
return await lifecycle.restart_existing_instance(session, instance, user, project_id)
|
return await lifecycle.restart_existing_instance(session, instance, user, project_id)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
@router.delete("/{project_id}/repositories/{repo_id}/instances/{instance_id}")
|
||||||
async def delete_instance(
|
async def delete_instance(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -194,8 +176,6 @@ async def delete_instance(
|
|||||||
"""Delete a tool instance."""
|
"""Delete a tool instance."""
|
||||||
instance = await _get_instance(session, instance_id, repo_id)
|
instance = await _get_instance(session, instance_id, repo_id)
|
||||||
await lifecycle.delete_existing_instance(session, instance)
|
await lifecycle.delete_existing_instance(session, instance)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs")
|
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs")
|
||||||
async def get_instance_logs(
|
async def get_instance_logs(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -211,8 +191,6 @@ async def get_instance_logs(
|
|||||||
if not instance.container_id:
|
if not instance.container_id:
|
||||||
return {"logs": "No container running"}
|
return {"logs": "No container running"}
|
||||||
return {"logs": container_svc.get_container_logs(instance.container_id, tail)}
|
return {"logs": container_svc.get_container_logs(instance.container_id, tail)}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/recreate-tunnel")
|
@router.post("/{project_id}/repositories/{repo_id}/instances/{instance_id}/recreate-tunnel")
|
||||||
async def recreate_tunnel_endpoint(
|
async def recreate_tunnel_endpoint(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -244,8 +222,6 @@ async def recreate_tunnel_endpoint(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Failed to recreate tunnel")
|
logger.exception("Failed to recreate tunnel")
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to recreate tunnel: {exc}")
|
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")
|
@router.get("/{project_id}/repositories/{repo_id}/instances/{instance_id}/health")
|
||||||
async def check_instance_tunnel_health(
|
async def check_instance_tunnel_health(
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
@@ -260,8 +236,6 @@ async def check_instance_tunnel_health(
|
|||||||
if not instance.url or instance.status != "running":
|
if not instance.url or instance.status != "running":
|
||||||
return {"healthy": False, "status_code": None, "error": "instance not running"}
|
return {"healthy": False, "status_code": None, "error": "instance not running"}
|
||||||
return tunnel_svc.check_tunnel_health(instance.url)
|
return tunnel_svc.check_tunnel_health(instance.url)
|
||||||
|
|
||||||
|
|
||||||
@router.api_route(
|
@router.api_route(
|
||||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/proxy/{path:path}",
|
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/proxy/{path:path}",
|
||||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
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}"
|
target_url = f"http://{instance.container_name}:{instance.port}"
|
||||||
if path:
|
if path:
|
||||||
target_url += f"/{path}"
|
target_url += f"/{path}"
|
||||||
query_string = str(request.query_params)
|
query = str(request.query_params)
|
||||||
if query_string:
|
if query:
|
||||||
target_url += f"?{query_string}"
|
target_url += f"?{query}"
|
||||||
|
|
||||||
headers = dict(request.headers)
|
headers = dict(request.headers)
|
||||||
headers.pop("host", None)
|
headers.pop("host", None)
|
||||||
@@ -304,8 +278,7 @@ async def proxy_to_instance(
|
|||||||
logger.error("Proxy error: %s", exc)
|
logger.error("Proxy error: %s", exc)
|
||||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"failed to reach instance: {exc}")
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"failed to reach instance: {exc}")
|
||||||
|
|
||||||
response_headers = dict(response.headers)
|
resp_headers = dict(response.headers)
|
||||||
for header in ["content-encoding", "transfer-encoding", "connection"]:
|
for h in ["content-encoding", "transfer-encoding", "connection"]:
|
||||||
response_headers.pop(header, None)
|
resp_headers.pop(h, None)
|
||||||
|
return Response(content=response.content, status_code=response.status_code, headers=resp_headers)
|
||||||
return Response(content=response.content, status_code=response.status_code, headers=response_headers)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user