fix: remove legacy config APIs

Remove stale ToolConfig and ConfigFolder backend/frontend surfaces after the ConfigProfile refactor. Drop dead routers, schemas, model exports, frontend routes, clients, pages, and tests; keep ToolType API compatibility for existing interface/is_builtin response shape.

Quality gates: backend LSP diagnostics passed; backend py_compile passed; backend ruff passed; frontend ToolWorkshopPage test passed. Frontend typecheck blocked by unrelated missing xterm-addon-serialize types.
This commit is contained in:
2026-06-03 12:40:47 +02:00
parent b6f89f9df0
commit 4201326467
33 changed files with 290 additions and 2932 deletions
+1 -2
View File
@@ -7,7 +7,7 @@ from .compose import (
write_compose_file,
write_env_file,
)
from .config_staging import write_config_files, write_config_folder_files
from .config_staging import write_config_files
from .container import (
connect_container_to_network,
find_free_port,
@@ -30,7 +30,6 @@ __all__ = [
"write_env_file",
"execute_compose_command",
"write_config_files",
"write_config_folder_files",
"get_container_id",
"get_container_name",
"connect_container_to_network",
+1 -58
View File
@@ -1,11 +1,7 @@
"""Config folder file staging for Docker instances."""
"""Config file staging for Docker instances."""
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
def write_config_files(instance_dir: str, files: dict[str, str]) -> None:
"""Write config files to the instance directory.
@@ -24,56 +20,3 @@ def write_config_files(instance_dir: str, files: dict[str, str]) -> None:
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.write_text(content)
def write_config_folder_files(instance_dir: str, folders: list, project_id: str | None = None) -> list[dict]:
"""Write config folder files to the instance directory and return volume mounts.
Args:
instance_dir: Path to instance directory
folders: List of ConfigFolder objects
project_id: Optional project ID for applying overrides
Returns:
List of volume mount dicts [{"source": "...", "target": "...", "type": "..."}]
"""
instance_path = Path(instance_dir)
volume_mounts = []
for folder in folders:
# Determine mount path (with project override if applicable)
mount_path = folder.mount_path
files = folder.files.copy()
if project_id and folder.project_overrides:
override = folder.project_overrides.get(str(project_id))
if override:
if override.get("mount_path"):
mount_path = override["mount_path"]
if override.get("files"):
files.update(override["files"])
# Write files to instance directory
folder_dir = instance_path / "volumes" / folder.name
folder_dir.mkdir(parents=True, exist_ok=True)
for file_path, content in files.items():
# Security: ensure path doesn't escape folder_dir
full_path = folder_dir / file_path
try:
full_path.resolve().relative_to(folder_dir.resolve())
except ValueError:
logger.warning("Config folder file path escapes directory: %s", file_path)
continue
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.write_text(content)
# Add volume mount
volume_mounts.append({
"source": str(folder_dir),
"target": mount_path,
"type": "bind",
})
return volume_mounts
+84 -86
View File
@@ -11,14 +11,11 @@ from datetime import datetime
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.models.config_folder import ConfigFolder
from src.models.config_profile import ConfigProfile
from src.models.git_repository import GitRepository
from src.models.project import Project
from src.models.tool_config import ToolConfig
from src.models.tool_instance import ToolInstance
from src.models.tool_type import ToolType
from src.models.user import User
@@ -54,7 +51,8 @@ async def create_new_instance(
instance = ToolInstance(
name=instance_name,
display_name=display_name or f"{project.name} / {repo.name} / {tool_type.display_name}",
display_name=display_name
or f"{project.name} / {repo.name} / {tool_type.display_name}",
tool_type_id=tool_type.id,
repository_id=repo.id,
project_id=project.id,
@@ -85,16 +83,27 @@ async def start_existing_instance(
instance.status = "building"
await session.commit()
env_vars, config_files, port_override, start_command, working_directory, _extra_env, extra_volumes = await _fetch_tool_configs(
session, user.id, instance.tool_type_id, project_id
)
env_vars: dict[str, str] = {}
config_files: dict[str, str] = {}
port_override = None
start_command = None
working_directory = None
extra_volumes: list[dict] = []
selected_profile = None
if instance.selected_profile_id:
selected_profile = await session.get(ConfigProfile, instance.selected_profile_id)
selected_profile = await session.get(
ConfigProfile, instance.selected_profile_id
)
if selected_profile and selected_profile.user_id == user.id:
instance_dir = os.path.dirname(instance.compose_path)
env_vars, port_override, start_command, working_directory, extra_volumes = await compose_svc._apply_resolved_profile(
(
env_vars,
port_override,
start_command,
working_directory,
extra_volumes,
) = await compose_svc._apply_resolved_profile(
selected_profile,
instance_dir,
env_vars,
@@ -104,14 +113,17 @@ async def start_existing_instance(
extra_volumes,
)
env_file_path, extra_volumes = await _stage_configs_and_folders(
session, user.id, project_id, os.path.dirname(instance.compose_path),
env_vars, config_files, extra_volumes
env_file_path, extra_volumes = await _stage_configs(
os.path.dirname(instance.compose_path), env_vars, config_files, extra_volumes
)
if port_override or start_command or working_directory or extra_volumes:
compose_svc._modify_compose_file(
instance.compose_path, port_override, start_command, working_directory, extra_volumes
instance.compose_path,
port_override,
start_command,
working_directory,
extra_volumes,
)
returncode, _stdout, stderr = compose_svc.execute_compose_command(
@@ -138,6 +150,14 @@ async def start_existing_instance(
await session.commit()
tool_type = await session.get(ToolType, instance.tool_type_id)
if not tool_type:
instance.status = "error"
await session.commit()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="tool type not found for instance",
)
success, probe_logs = await _run_readiness_probe(instance, tool_type)
if not success:
instance.status = "failed"
@@ -175,27 +195,45 @@ async def restart_existing_instance(
await session.commit()
return {"status": instance.status}
env_vars, config_files, port_override, start_command, working_directory, _extra_env, extra_volumes = await _fetch_tool_configs(
session, user.id, instance.tool_type_id, project_id
)
env_vars: dict[str, str] = {}
config_files: dict[str, str] = {}
port_override = None
start_command = None
working_directory = None
extra_volumes: list[dict] = []
stored_profile = None
if instance.selected_profile_id:
stored_profile = await session.get(ConfigProfile, instance.selected_profile_id)
if stored_profile and stored_profile.user_id == user.id:
instance_dir = os.path.dirname(instance.compose_path)
env_vars, port_override, start_command, working_directory, extra_volumes = await compose_svc._apply_resolved_profile(
stored_profile, instance_dir, env_vars, port_override, start_command, working_directory, extra_volumes
(
env_vars,
port_override,
start_command,
working_directory,
extra_volumes,
) = await compose_svc._apply_resolved_profile(
stored_profile,
instance_dir,
env_vars,
port_override,
start_command,
working_directory,
extra_volumes,
)
env_file_path, extra_volumes = await _stage_configs_and_folders(
session, user.id, project_id, os.path.dirname(instance.compose_path),
env_vars, config_files, extra_volumes
env_file_path, extra_volumes = await _stage_configs(
os.path.dirname(instance.compose_path), env_vars, config_files, extra_volumes
)
if port_override or start_command or working_directory or extra_volumes:
compose_svc._modify_compose_file(
instance.compose_path, port_override, start_command, working_directory, extra_volumes
instance.compose_path,
port_override,
start_command,
working_directory,
extra_volumes,
)
returncode, _stdout, _stderr = compose_svc.execute_compose_command(
@@ -210,6 +248,11 @@ async def restart_existing_instance(
instance.last_started_at = datetime.now()
tool_type = await session.get(ToolType, instance.tool_type_id)
if not tool_type:
instance.status = "error"
await session.commit()
return {"status": instance.status}
await _start_tunnel_if_web(instance, tool_type)
await session.commit()
@@ -235,7 +278,9 @@ async def stop_existing_instance(session: AsyncSession, instance: ToolInstance)
await session.commit()
async def delete_existing_instance(session: AsyncSession, instance: ToolInstance) -> None:
async def delete_existing_instance(
session: AsyncSession, instance: ToolInstance
) -> None:
"""Delete an instance, its containers, and its directory."""
if instance.tunnel_id:
try:
@@ -255,6 +300,7 @@ async def delete_existing_instance(session: AsyncSession, instance: ToolInstance
# ── Internal helpers ───────────────────────────────────────────────────────
async def _build_or_render_compose(
tool_type: ToolType,
instance_name: str,
@@ -283,11 +329,11 @@ async def _build_or_render_compose(
compose_content = (
f'version: "3.8"\nservices:\n app:\n'
f' image: {image_tag}\n'
f' container_name: {instance_name}\n'
f" image: {image_tag}\n"
f" container_name: {instance_name}\n"
f' ports:\n - "{tool_port}:{tool_type.default_port}"\n'
f' volumes:\n - {repo.path}:/workspace\n'
f' restart: unless-stopped\n'
f" volumes:\n - {repo.path}:/workspace\n"
f" restart: unless-stopped\n"
)
else:
variables = {
@@ -299,6 +345,11 @@ async def _build_or_render_compose(
"USER_ID": str(user.id),
"PROJECT_ID": str(project_id),
}
if not tool_type.compose_template:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="tool type has no compose template",
)
compose_content = compose_svc.render_compose_template(
tool_type.compose_template, variables
)
@@ -307,80 +358,25 @@ async def _build_or_render_compose(
return os.path.join(instance_dir, "docker-compose.yml")
async def _fetch_tool_configs(
session: AsyncSession,
user_id: Any,
tool_type_id: Any,
project_id: Any,
) -> tuple[dict, dict, Any, Any, Any, dict, list]:
"""Fetch tool configs and return parsed values."""
env_vars: dict[str, str] = {}
config_files: dict[str, str] = {}
port_override = None
start_command = None
working_directory = None
extra_env_vars: dict[str, str] = {}
extra_volumes: list[dict] = []
query = (
select(ToolConfig)
.where(ToolConfig.user_id == user_id, ToolConfig.tool_type_id == tool_type_id)
.where((ToolConfig.project_id == project_id) | (ToolConfig.project_id.is_(None)))
)
configs = (await session.execute(query)).scalars().all()
for cfg in configs:
if cfg.config_type == "env":
env_vars[cfg.key] = cfg.value
elif cfg.config_type == "file" and cfg.file_path:
config_files[cfg.file_path] = cfg.value
if cfg.port_override:
port_override = cfg.port_override
if cfg.start_command:
start_command = cfg.start_command
if cfg.working_directory:
working_directory = cfg.working_directory
if cfg.environment_variables:
extra_env_vars.update(cfg.environment_variables)
if cfg.volumes:
extra_volumes.extend(cfg.volumes)
env_vars.update(extra_env_vars)
return env_vars, config_files, port_override, start_command, working_directory, extra_env_vars, extra_volumes
async def _stage_configs_and_folders(
session: AsyncSession,
user_id: Any,
project_id: Any,
async def _stage_configs(
instance_dir: str,
env_vars: dict[str, str],
config_files: dict[str, str],
extra_volumes: list[dict],
) -> tuple[str | None, list[dict]]:
"""Write env/config files and config folders."""
"""Write env/config files for the resolved profile."""
env_file_path: str | None = None
if env_vars:
env_file_path = compose_svc.write_env_file(instance_dir, env_vars)
if config_files:
config_staging.write_config_files(instance_dir, config_files)
folder_query = select(ConfigFolder).where(
ConfigFolder.user_id == user_id, ConfigFolder.is_active.is_(True)
)
folders = (await session.execute(folder_query)).scalars().all()
if folders:
folder_volumes = config_staging.write_config_folder_files(
instance_dir, folders, str(project_id)
)
extra_volumes.extend(folder_volumes)
return env_file_path, extra_volumes
async def _start_tunnel_if_web(instance: ToolInstance, tool_type: ToolType) -> None:
"""Create Cloudflare tunnel for web-enabled tools."""
if "web" not in tool_type.interfaces or not tool_type.default_port:
if tool_type.interface_type != "web" or not tool_type.default_port:
instance.url = None
instance.public_url = None
return
@@ -393,7 +389,9 @@ async def _start_tunnel_if_web(instance: ToolInstance, tool_type: ToolType) -> N
instance.tunnel_id = tunnel_info["pid"]
instance.public_url = tunnel_info["url"]
instance.url = tunnel_info["url"]
logger.info("Created tunnel for instance %s: %s", instance.id, tunnel_info["url"])
logger.info(
"Created tunnel for instance %s: %s", instance.id, tunnel_info["url"]
)
except Exception as exc:
logger.error("Failed to create tunnel for instance %s: %s", instance.id, exc)
instance.status = "error"