""" Docker клиент для управления контейнерами Автор: Сергей Антропов Сайт: https://devops.org.ru """ import docker import os from typing import List, Dict, Optional from app.core.config import settings import logging logger = logging.getLogger(__name__) class DockerClient: """Клиент для работы с Docker API""" def __init__(self): """Инициализация Docker клиента (ленивая инициализация)""" self._client = None @property def client(self): """Ленивая инициализация Docker клиента""" if self._client is None: try: # Получаем DOCKER_HOST из настроек или окружения docker_host = os.getenv("DOCKER_HOST", settings.DOCKER_HOST) logger.info(f"Initializing Docker client with DOCKER_HOST: {docker_host}") # Если DOCKER_HOST начинается с unix://, извлекаем путь к socket if docker_host.startswith("unix://"): socket_path = docker_host.replace("unix://", "") # Убеждаемся, что путь начинается с / if not socket_path.startswith("/"): socket_path = "/" + socket_path # Docker SDK для unix socket ожидает base_url в формате "unix:///path/to/socket" # Важно: после unix:// должно быть три слэша (unix:///) # Например: "unix:///var/run/docker.sock" base_url = f"unix://{socket_path}" logger.info(f"Using unix socket: base_url={base_url}, socket_path={socket_path}") # НЕ используем docker.from_env() для unix socket, так как он неправильно парсит формат # Используем только прямой base_url try: self._client = docker.DockerClient(base_url=base_url) # Проверяем подключение сразу self._client.ping() logger.info(f"Successfully created Docker client with base_url={base_url}") except Exception as e: logger.error(f"Failed to create Docker client with base_url={base_url}: {e}") # Пробуем альтернативный формат (без префикса unix://) try: # Некоторые версии SDK могут требовать просто путь # Но это не работает, так как base_url должен быть полный URL # Поэтому пробуем стандартный формат по умолчанию logger.warning("Trying default socket path") self._client = docker.DockerClient(base_url="unix:///var/run/docker.sock") self._client.ping() logger.info("Successfully created Docker client with default socket") except Exception as e2: logger.error(f"All methods failed. Last error: {e2}") raise elif docker_host.startswith("/"): # Прямой путь к socket - используем base_url с префиксом unix:// base_url = f"unix://{docker_host}" logger.info(f"Using direct socket path: {base_url}") self._client = docker.DockerClient(base_url=base_url) else: # Для других форматов (tcp://, http:// и т.д.) используем from_env # Но сначала проверяем, не установлена ли переменная DOCKER_HOST if "DOCKER_HOST" in os.environ: # Если DOCKER_HOST установлен, но не unix://, используем from_env logger.info("Using docker.from_env()") self._client = docker.from_env() else: # Если DOCKER_HOST не установлен, используем стандартный socket logger.info("Using default socket: unix:///var/run/docker.sock") self._client = docker.DockerClient(base_url="unix:///var/run/docker.sock") # Проверка подключения self._client.ping() logger.info("Docker client initialized successfully") except Exception as e: logger.error(f"Failed to initialize Docker client: {e}") logger.error(f"DOCKER_HOST env: {os.getenv('DOCKER_HOST', 'not set')}") logger.error(f"Settings DOCKER_HOST: {settings.DOCKER_HOST}") import traceback logger.error(traceback.format_exc()) raise return self._client def list_images(self) -> List[Dict]: """Список всех Docker образов""" try: images = self.client.images.list() return [ { "id": img.id, "tags": img.tags, "created": img.attrs.get("Created"), "size": img.attrs.get("Size", 0) } for img in images ] except Exception as e: logger.error(f"Error listing images: {e}") return [] def list_containers(self, all: bool = False) -> List[Dict]: """Список контейнеров""" try: containers = self.client.containers.list(all=all) return [ { "id": container.id, "name": container.name, "status": container.status, "image": container.image.tags[0] if container.image.tags else container.image.id, "ports": container.ports, "created": container.attrs.get("Created") } for container in containers ] except Exception as e: logger.error(f"Error listing containers: {e}") return [] def get_container_ip(self, container_name: str) -> Optional[str]: """Получение IP адреса контейнера""" try: container = self.client.containers.get(container_name) network_settings = container.attrs.get("NetworkSettings", {}) networks = network_settings.get("Networks", {}) # Ищем IP в первой доступной сети for network_name, network_info in networks.items(): ip = network_info.get("IPAddress") if ip: return ip return None except Exception as e: logger.error(f"Error getting container IP for {container_name}: {e}") return None def create_container( self, image: str, name: str, command: Optional[str] = None, environment: Optional[Dict] = None, volumes: Optional[Dict] = None, network: Optional[str] = None, privileged: bool = False ) -> Dict: """Создание контейнера""" try: container = self.client.containers.run( image=image, name=name, command=command, environment=environment or {}, volumes=volumes or {}, network=network, privileged=privileged, detach=True, remove=False ) return { "success": True, "id": container.id, "name": container.name, "status": container.status } except Exception as e: logger.error(f"Error creating container: {e}") return { "success": False, "error": str(e) } def stop_container(self, container_name: str) -> bool: """Остановка контейнера""" try: container = self.client.containers.get(container_name) container.stop() return True except Exception as e: logger.error(f"Error stopping container {container_name}: {e}") return False def remove_container(self, container_name: str, force: bool = False) -> bool: """Удаление контейнера""" try: container = self.client.containers.get(container_name) container.remove(force=force) return True except Exception as e: logger.error(f"Error removing container {container_name}: {e}") return False def get_container_logs(self, container_name: str, tail: int = 100) -> str: """Получение логов контейнера""" try: container = self.client.containers.get(container_name) logs = container.logs(tail=tail, timestamps=True) return logs.decode('utf-8', errors='replace') except Exception as e: logger.error(f"Error getting logs for {container_name}: {e}") return "" def exec_command(self, container_name: str, command: str) -> Dict: """Выполнение команды в контейнере""" try: container = self.client.containers.get(container_name) result = container.exec_run(command) return { "success": result.exit_code == 0, "output": result.output.decode('utf-8', errors='replace'), "exit_code": result.exit_code } except Exception as e: logger.error(f"Error executing command in {container_name}: {e}") return { "success": False, "error": str(e) }