feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
231
app/core/docker_client.py
Normal file
231
app/core/docker_client.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user