refactor: slim config_profiles router to 301 lines

Extract CRUD helpers into services/config/crud_service.py.
Move instance-related config logic to services/tool/instance_service.py.

Quality gates: py_compile pass
This commit is contained in:
Developer
2026-06-05 20:56:50 +00:00
parent 88c56a83b7
commit 6efe524974
3 changed files with 511 additions and 185 deletions
+305 -3
View File
@@ -398,9 +398,6 @@ def expand_glob_source(source_path: str, repo_path: str) -> list[str]:
return results
router = APIRouter(prefix="/projects", tags=["tool-instances"])
async def validate_config_profile(
session: AsyncSession,
profile_id: str | None,
@@ -880,3 +877,308 @@ async def prepare_manifest_instance(
return image_tag, compose_content, manifest, home_dir
async def create_tool_instance(
session: AsyncSession,
user_id: uuid.UUID,
project_id: uuid.UUID,
repo_id: uuid.UUID,
data: "CreateInstanceRequest",
) -> ToolInstance:
"""Create a new tool instance for a repository.
Returns the created ToolInstance.
Raises ValueError for invalid input, RuntimeError for internal failures.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise ValueError("repository not found")
tool_type_id = uuid.UUID(data.tool_type_id)
tool_type = await session.get(ToolType, tool_type_id)
if tool_type is None:
raise ValueError("tool type not found")
# Validate config profile if provided
selected_profile_id = await validate_config_profile(
session, data.config_profile_id, user_id, project_id, tool_type_id
)
# Resolve workspace if provided
workspace = None
workspace_id = None
if data.workspace_id:
from src.models import Workspace as WorkspaceModel
try:
workspace_id = uuid.UUID(data.workspace_id)
except ValueError:
raise ValueError("Invalid workspace_id format")
workspace = await session.get(WorkspaceModel, workspace_id)
if workspace is None:
raise ValueError("workspace not found")
if workspace.repo_id != repo_id:
raise ValueError("workspace does not belong to this repository")
# Validate clone mode requirements (legacy path)
if data.clone_mode == "clone" and not workspace:
if not repo.remote_url:
raise ValueError("repository does not have a remote URL for cloning")
if not repo.ssh_key_id:
raise ValueError("repository must have an SSH key assigned for clone mode")
# Generate unique name
instance_name = f"{tool_type.name}-{repo.name}-{uuid.uuid4().hex[:8]}"
# Auto-generate display name with scoped numbering.
if data.display_name:
instance_display = data.display_name
else:
scope_name = workspace.name if workspace else repo.name
auto_name = f"{scope_name} / {tool_type.display_name}"
if workspace:
count_query = (
select(ToolInstance)
.where(ToolInstance.workspace_id == workspace_id)
.where(ToolInstance.tool_type_id == tool_type_id)
.where(ToolInstance.owner_id == user_id)
)
else:
count_query = (
select(ToolInstance)
.where(ToolInstance.repository_id == repo_id)
.where(ToolInstance.tool_type_id == tool_type_id)
.where(ToolInstance.owner_id == user_id)
)
result = await session.execute(count_query)
existing_count = len(result.scalars().all())
if existing_count > 0:
instance_display = f"{auto_name} #{existing_count + 1}"
else:
instance_display = auto_name
# Create instance directory
instance_dir = ensure_instance_directory(instance_name)
compose_path = os.path.join(instance_dir, "docker-compose.yml")
# Find free port
tool_port = find_free_port()
# Determine repo path based on workspace or clone mode
if workspace:
repo_path = workspace.path
elif data.clone_mode == "clone":
ssh_key = await session.get(SSHKey, repo.ssh_key_id)
if ssh_key is None:
raise ValueError("repository SSH key not found")
ssh_key_path = None
try:
ssh_dir = prepare_ssh_key_files(instance_dir, ssh_key)
ssh_key_path = os.path.join(ssh_dir, "id_ed25519")
clone_path = clone_repository(
remote_url=repo.remote_url,
ssh_key_path=ssh_key_path,
instance_dir=instance_dir,
branch=data.branch or "main",
)
repo_path = clone_path
except Exception as exc:
logger.exception("Failed to clone repository: %s", exc)
cleanup_ssh_key_files(instance_dir)
raise RuntimeError(f"Failed to clone repository: {exc}")
else:
repo_path = repo.path
# Verify cloned repo has files
if data.clone_mode == "clone" and repo_path:
try:
repo_contents = os.listdir(repo_path)
if not repo_contents or (
len(repo_contents) == 1 and repo_contents[0] == ".git"
):
logger.error("Cloned repository at %s appears empty", repo_path)
raise RuntimeError("Cloned repository is empty")
logger.debug(
"Verified cloned repo at %s has %d items",
repo_path,
len(repo_contents),
)
except Exception as exc:
logger.exception("Failed to verify cloned repository: %s", exc)
raise RuntimeError(f"Cloned repository verification failed: {exc}")
# Create new local branch if requested
if data.clone_mode == "clone" and data.new_branch:
try:
result = subprocess.run(
["git", "-C", repo_path, "checkout", "-b", data.new_branch],
capture_output=True,
text=True,
)
if result.returncode != 0:
logger.error(
"Failed to create branch %s: %s", data.new_branch, result.stderr
)
raise RuntimeError(f"Failed to create branch: {result.stderr}")
logger.debug(
"Created local branch %s in cloned repository", data.new_branch
)
except Exception as exc:
logger.exception("Failed to create local branch: %s", exc)
raise RuntimeError(f"Failed to create local branch: {exc}")
# Handle based on definition type
if tool_type.definition_type == "dockerfile":
image_tag = f"headquarter/{instance_name}:latest".lower()
if tool_type.dockerfile_template:
returncode, stdout, stderr = await asyncio.to_thread(
build_image,
instance_dir=instance_dir,
dockerfile=tool_type.dockerfile_template,
tag=image_tag,
build_context=tool_type.build_context,
)
if returncode != 0:
logger.error(
"Failed to build image for instance %s: %s",
instance_name,
stderr,
)
raise RuntimeError(f"Failed to build Docker image: {stderr[:500]}")
logger.info(
"Successfully built image %s for instance %s",
image_tag,
instance_name,
)
ports_section = (
f""" ports:\n - "{tool_port}:{tool_type.default_port}"\n"""
if tool_type.default_port and tool_type.default_port > 0
else ""
)
compose_content = f"""version: "3.8"\nservices:\n app:\n image: {image_tag}\n container_name: {instance_name.lower()}\n stdin_open: true\n tty: true\n{ports_section} volumes:\n - {repo_path}:/workspace\n restart: unless-stopped\n"""
write_compose_file(instance_dir, compose_content)
elif tool_type.definition_type == "manifest":
from src.models import ToolDefinitionManifest
manifest_def = await session.get(
ToolDefinitionManifest, tool_type.manifest_id
)
if not manifest_def:
raise RuntimeError("Manifest definition not found for this tool type")
manifest = dict(manifest_def.manifest)
if manifest_def.base_definition_id:
base_def = await session.get(
ToolDefinitionManifest, manifest_def.base_definition_id
)
if base_def:
manifest = resolve_base(
deep_merge(dict(base_def.manifest), manifest)
)
image_tag = compute_image_tag(tool_type.name, manifest)
variables = {
"IMAGE_TAG": image_tag,
"INSTANCE_NAME": instance_name.lower(),
"INSTANCE_DIR": instance_dir,
"REPO_PATH": repo_path,
"SSH_PATH": "",
"TOOL_PORT": tool_port,
"EXTRA_ENV": {},
"EXTRA_VOLUMES": [],
}
compose_content = compile_compose(manifest, variables)
write_compose_file(instance_dir, compose_content)
else:
variables = {
"REPO_PATH": repo_path,
"INSTANCE_NAME": instance_name,
"INSTANCE_ID": instance_name,
"TOOL_NAME": instance_name,
"TOOL_PORT": tool_port,
"USER_ID": str(user_id),
"PROJECT_ID": str(project_id),
}
compose_content = render_compose_template(
tool_type.compose_template, variables
)
if data.clone_mode == "clone" and repo_path:
import yaml
compose_data = yaml.safe_load(compose_content)
repo_mounted = False
if compose_data and "services" in compose_data:
for svc in compose_data["services"].values():
volumes = svc.get("volumes", [])
for vol in volumes:
vol_str = str(vol)
if repo_path in vol_str:
repo_mounted = True
break
if repo_mounted:
break
if not repo_mounted:
logger.warning(
"Compose template for tool type %s does not mount repo path; adding default mount",
tool_type.name,
)
if compose_data and "services" in compose_data:
for svc in compose_data["services"].values():
if "volumes" not in svc:
svc["volumes"] = []
svc["volumes"].append(f"{repo_path}:/workspace")
break
compose_content = yaml.dump(
compose_data, default_flow_style=False
)
write_compose_file(instance_dir, compose_content)
# Create database record
instance = ToolInstance(
name=instance_name,
display_name=instance_display,
tool_type_id=tool_type_id,
repository_id=repo_id,
project_id=project_id,
owner_id=user_id,
status="pending",
compose_path=compose_path,
port=tool_port,
workspace_id=workspace_id,
clone_mode=data.clone_mode,
branch=data.new_branch
if data.new_branch
else (data.branch if data.clone_mode == "clone" else None),
selected_config_profile_id=selected_profile_id,
ssh_key_ids=data.ssh_key_ids or None,
)
session.add(instance)
await session.commit()
await session.refresh(instance)
await publish_lifecycle_event(
event_bus=_event_bus,
session=session,
instance=instance,
event_type="instance.created",
created_by=user_id,
status="pending",
message="Instance created",
)
return instance