import asyncio from pathlib import Path import yaml from app.integrations.docker.compose import ( ComposeDocument, find_docker_compose, run_docker_command, ) from app.integrations.docker.ports import _get_port_index, _replace_port_in_mapping def _render_compose(document: ComposeDocument) -> str: return yaml.dump(document, default_flow_style=False, sort_keys=False) def _write_compose(path: Path, content: str) -> None: path.write_text(content, encoding="utf-8") def _read_compose_text(path: Path) -> str: return path.read_text(encoding="utf-8") async def change_port( service_name: str, folder: Path, port_from: int, port_to: int, host_to: str | None = None, rewrite_host: bool = False, ) -> bool: compose_data = find_docker_compose(folder) if compose_data is None: return False docker_compose, compose_path = compose_data compose_services = docker_compose.get("services") or {} if not isinstance(compose_services, dict) or not compose_services: return False service_config = compose_services.get(service_name) if not isinstance(service_config, dict): return False mappings = service_config.get("ports") if not isinstance(mappings, list): return False idx = _get_port_index(mappings, port_from) if idx is None: return False mappings[idx] = _replace_port_in_mapping( mappings[idx], port_from, port_to, host_to=host_to, rewrite_host=rewrite_host, ) original_text = await asyncio.to_thread(_read_compose_text, compose_path) updated_text = _render_compose(docker_compose) try: await asyncio.to_thread(_write_compose, compose_path, updated_text) result = await run_docker_command( compose_path, ["up", "-d", service_name], ) except Exception: try: await asyncio.to_thread(_write_compose, compose_path, original_text) except OSError as restore_error: raise RuntimeError( f"Failed to restore original compose file: {compose_path}" ) from restore_error return False if result is None or result.get("status") != "success": await asyncio.to_thread(_write_compose, compose_path, original_text) return False return True