Merge branch 'main' of ssh://git.commumedia.org:2222/alex/headquarter
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user