Files
DevOpsLab/app/core/docker_client.py
Сергей Антропов 1fbf9185a2 feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
2026-02-15 22:59:02 +03:00

232 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)
}