3.2 Launch and restart profile application (el-5z8)

This commit is contained in:
2026-05-24 14:41:17 +00:00
parent 13aceeb08d
commit 9cc98455ef
+186 -16
View File
@@ -42,6 +42,7 @@ from src.services.docker import (
write_config_folder_files,
)
from src.services.docker_build import build_image
from src.services.profile_resolver import resolve_profile
from src.services.readiness_probe import execute_probe
router = APIRouter(prefix="/projects", tags=["tool-instances"])
@@ -109,6 +110,68 @@ def _modify_compose_file(
compose_file.write_text(yaml.dump(compose_data, default_flow_style=False))
async def _apply_resolved_profile(
profile: ConfigProfile,
instance_dir: str,
env_vars: dict[str, str],
port_override: int | None,
start_command: str | None,
working_directory: str | None,
extra_volumes: list[dict],
) -> tuple[dict[str, str], int | None, str | None, str | None, list[dict]]:
"""Resolve a profile and apply its output to instance configuration.
Merges resolved profile env vars (profile wins), applies runtime hints,
stages mount files to the instance directory, and adds Docker bind mounts.
Args:
profile: The config profile to resolve and apply.
instance_dir: Path to the instance directory.
env_vars: Current environment variables dict (will be updated).
port_override: Current port override (may be updated).
start_command: Current start command (may be updated).
working_directory: Current working directory (may be updated).
extra_volumes: Current extra volumes list (will be extended).
Returns:
Updated (env_vars, port_override, start_command, working_directory, extra_volumes).
"""
from pathlib import Path
resolved = resolve_profile(profile)
# Merge env vars from resolved profile (profile wins over tool configs)
if resolved.environment_variables:
env_vars.update(resolved.environment_variables)
# Apply runtime hints
if resolved.runtime_hints.start_command is not None:
start_command = resolved.runtime_hints.start_command
if resolved.runtime_hints.working_directory is not None:
working_directory = resolved.runtime_hints.working_directory
if resolved.runtime_hints.port is not None:
port_override = resolved.runtime_hints.port
# Stage mount files and add volume mounts
for target_path, mount in resolved.mounts.items():
safe_name = target_path.strip("/").replace("/", "_")
mount_dir = Path(instance_dir) / "mounts" / safe_name
mount_dir.mkdir(parents=True, exist_ok=True)
for rel_path, content in mount.files.items():
file_path = mount_dir / rel_path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
extra_volumes.append({
"source": str(mount_dir),
"target": target_path,
"type": mount.mode,
})
return env_vars, port_override, start_command, working_directory, extra_volumes
async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
"""Fetch a user by ID or raise 404 if not found."""
user = await session.get(User, user_id)
@@ -481,21 +544,6 @@ async def start_instance(
extra_env_vars = {}
extra_volumes = []
if instance.selected_profile_id:
# Validate the selected config profile
selected_profile = await session.get(ConfigProfile, instance.selected_profile_id)
if selected_profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="config profile not found",
)
if selected_profile.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="config profile does not belong to user",
)
logger.info("Using selected config profile %s for instance %s", instance.selected_profile_id, instance.id)
# Fetch all matching configs for this tool type
config_query = select(ToolConfig).where(
ToolConfig.user_id == user_id,
@@ -529,6 +577,31 @@ async def start_instance(
# Merge extra env vars
env_vars.update(extra_env_vars)
# Apply resolved profile output if a profile is selected
if instance.selected_profile_id:
selected_profile = await session.get(ConfigProfile, instance.selected_profile_id)
if selected_profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="config profile not found",
)
if selected_profile.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="config profile does not belong to user",
)
instance_dir = os.path.dirname(instance.compose_path)
env_vars, port_override, start_command, working_directory, extra_volumes = await _apply_resolved_profile(
selected_profile,
instance_dir,
env_vars,
port_override,
start_command,
working_directory,
extra_volumes,
)
logger.info("Applied resolved profile %s for instance %s", selected_profile.name, instance.id)
# Fetch active config folders for this user
folder_query = select(ConfigFolder).where(
ConfigFolder.user_id == user_id,
@@ -800,8 +873,105 @@ async def restart_instance(
logger.warning("Failed to stop old tunnel for instance %s: %s", instance.id, exc)
if instance.compose_path and os.path.exists(instance.compose_path):
# Re-apply configuration using stored profile instead of current defaults
env_vars = {}
config_files = {}
port_override = None
start_command = None
working_directory = None
extra_env_vars = {}
extra_volumes = []
# Fetch all matching configs for this tool type
config_query = select(ToolConfig).where(
ToolConfig.user_id == user_id,
ToolConfig.tool_type_id == instance.tool_type_id,
).where(
(ToolConfig.project_id == project_id) | (ToolConfig.project_id.is_(None))
)
config_result = await session.execute(config_query)
configs = config_result.scalars().all()
logger.info("Found %d tool configs for restart of instance %s", len(configs), instance.id)
for config in configs:
if config.config_type == "env":
env_vars[config.key] = config.value
elif config.config_type == "file" and config.file_path:
config_files[config.file_path] = config.value
if config.port_override:
port_override = config.port_override
if config.start_command:
start_command = config.start_command
if config.working_directory:
working_directory = config.working_directory
if config.environment_variables:
extra_env_vars.update(config.environment_variables)
if config.volumes:
extra_volumes.extend(config.volumes)
# Merge extra env vars
env_vars.update(extra_env_vars)
# Apply stored profile on restart instead of current defaults
if instance.selected_profile_id:
stored_profile = await session.get(ConfigProfile, instance.selected_profile_id)
if stored_profile is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="config profile not found",
)
if stored_profile.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="config profile does not belong to user",
)
instance_dir = os.path.dirname(instance.compose_path)
env_vars, port_override, start_command, working_directory, extra_volumes = await _apply_resolved_profile(
stored_profile,
instance_dir,
env_vars,
port_override,
start_command,
working_directory,
extra_volumes,
)
logger.info("Re-applied stored profile %s for restart of instance %s", stored_profile.name, instance.id)
# Fetch active config folders for this user
folder_query = select(ConfigFolder).where(
ConfigFolder.user_id == user_id,
ConfigFolder.is_active == True,
)
folder_result = await session.execute(folder_query)
config_folders = folder_result.scalars().all()
# Write env file and config files
instance_dir = os.path.dirname(instance.compose_path)
env_file_path = None
if env_vars:
env_file_path = write_env_file(instance_dir, env_vars)
logger.info("Wrote env file for restart of instance %s: %s", instance.id, env_file_path)
if config_files:
write_config_files(instance_dir, config_files)
logger.info("Wrote %d config files for restart of instance %s", len(config_files), instance.id)
# Write config folder files
if config_folders:
folder_volumes = write_config_folder_files(instance_dir, config_folders, str(project_id))
extra_volumes.extend(folder_volumes)
logger.info("Wrote config folders with %d volume mounts for restart of instance %s", len(folder_volumes), instance.id)
# Modify compose file if needed
if port_override or start_command or working_directory or extra_volumes:
_modify_compose_file(instance.compose_path, port_override, start_command, working_directory, extra_volumes)
logger.info("Modified compose file for restart of instance %s", instance.id)
returncode, stdout, stderr = execute_compose_command(
instance.compose_path, "restart"
instance.compose_path, "restart", env_file=env_file_path
)
if returncode == 0: