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

237 lines
9.7 KiB
Python
Raw Permalink 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 Builder API
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from typing import Optional, List
import httpx
logger = logging.getLogger(__name__)
class DockerBuilderService:
"""Сервис для работы с Docker Builder API"""
def __init__(self, base_url: str = "http://docker-builder:8001"):
"""
Инициализация сервиса
Args:
base_url: URL Docker Builder API
"""
self.base_url = base_url.rstrip("/")
async def start_build(
self,
build_log_id: int,
dockerfile_id: int = None, # ID Dockerfile в БД (приоритет)
dockerfile_content: str = None, # Для обратной совместимости
dockerfile_path: str = None, # Для обратной совместимости
image_name: str = None,
tag: str = None,
platforms: List[str] = None,
context_path: str = None,
no_cache: bool = False,
webhook_url: Optional[str] = None
) -> dict:
"""
Запуск сборки Docker образа через Builder API
Args:
build_log_id: ID записи лога в БД
dockerfile_content: Содержимое Dockerfile (приоритет над dockerfile_path)
dockerfile_path: Путь к Dockerfile (если dockerfile_content не указан)
image_name: Имя образа
tag: Тег образа
platforms: Список платформ для сборки
context_path: Путь к контексту сборки (если не указан, будет создана временная директория)
no_cache: Сборка без кеша
webhook_url: URL для отправки логов обратно в основное приложение
Returns:
dict: Информация о запущенной сборке
"""
url = f"{self.base_url}/api/v1/build/start"
payload = {
"build_log_id": build_log_id,
"image_name": image_name,
"tag": tag,
"platforms": platforms,
"no_cache": no_cache,
}
# Добавляем webhook_url только если он указан
if webhook_url:
payload["webhook_url"] = webhook_url
# Добавляем dockerfile_id (приоритет) или dockerfile_content/dockerfile_path для обратной совместимости
if dockerfile_id:
payload["dockerfile_id"] = dockerfile_id
elif dockerfile_content:
payload["dockerfile_content"] = dockerfile_content
elif dockerfile_path:
payload["dockerfile_path"] = dockerfile_path
# Если передан context_path, используем его
if context_path:
payload["context_path"] = context_path
try:
async with httpx.AsyncClient(timeout=30.0) as client:
logger.info(f"Sending build request to {url} with payload: {payload}")
response = await client.post(url, json=payload)
# Если ошибка, логируем детали ответа
if response.status_code != 200:
error_detail = response.text
logger.error(f"Builder API returned {response.status_code}: {error_detail}")
try:
error_json = response.json()
error_detail = error_json.get("detail", error_detail)
except:
pass
raise Exception(f"Builder API вернул ошибку {response.status_code}: {error_detail}")
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
logger.error(f"Error starting build via Builder API: {e}")
if hasattr(e, 'response') and e.response is not None:
try:
error_detail = e.response.json()
logger.error(f"Error details: {error_detail}")
except:
logger.error(f"Error response text: {e.response.text}")
raise Exception(f"Не удалось запустить сборку через Builder API: {e}")
async def get_build_status(self, build_id: str) -> dict:
"""
Получение статуса сборки
Args:
build_id: ID сборки
Returns:
dict: Статус сборки
"""
url = f"{self.base_url}/api/v1/build/{build_id}/status"
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(url)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
logger.error(f"Error getting build status: {e}")
raise Exception(f"Не удалось получить статус сборки: {e}")
async def cancel_build(self, build_id: str) -> dict:
"""
Отмена сборки
Args:
build_id: ID сборки
Returns:
dict: Результат отмены
"""
url = f"{self.base_url}/api/v1/build/{build_id}/cancel"
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(url)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
logger.error(f"Error cancelling build: {e}")
raise Exception(f"Не удалось отменить сборку: {e}")
async def push_image(
self,
image_name: str,
webhook_url: Optional[str] = None,
registry: Optional[str] = "docker.io",
username: Optional[str] = None,
password: Optional[str] = None
) -> dict:
"""
Отправка Docker образа в registry через Builder API
Args:
image_name: Полное имя образа с тегом (например, "myimage:latest")
webhook_url: URL для отправки логов обратно в основное приложение
registry: Registry (docker.io или URL Harbor)
username: Имя пользователя для авторизации
password: Пароль для авторизации
Returns:
dict: Информация о запущенной отправке
"""
url = f"{self.base_url}/api/v1/push/start"
payload = {
"image_name": image_name,
"webhook_url": webhook_url,
"registry": registry,
"username": username,
"password": password
}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(url, json=payload)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
# Обрабатываем ошибки HTTP статуса
error_detail = f"HTTP {e.response.status_code}"
try:
error_data = e.response.json()
if isinstance(error_data, dict):
if "detail" in error_data:
detail = error_data["detail"]
if isinstance(detail, list):
error_detail = ", ".join([str(item) for item in detail])
else:
error_detail = str(detail)
elif "message" in error_data:
error_detail = str(error_data["message"])
elif isinstance(error_data, list):
error_detail = ", ".join([str(item) for item in error_data])
except:
# Если не удалось распарсить JSON, используем текст ответа
try:
error_detail = e.response.text[:500] # Ограничиваем длину
except:
error_detail = str(e)
logger.error(f"Error starting push via Builder API: {error_detail}")
raise Exception(f"Не удалось запустить отправку образа через Builder API: {error_detail}")
except httpx.HTTPError as e:
logger.error(f"Error starting push via Builder API: {e}")
raise Exception(f"Не удалось запустить отправку образа через Builder API: {str(e)}")
async def health_check(self) -> dict:
"""
Проверка здоровья Builder API
Returns:
dict: Статус здоровья
"""
url = f"{self.base_url}/health"
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(url)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
logger.warning(f"Builder API health check failed: {e}")
return {"status": "unhealthy", "error": str(e)}
# Глобальный экземпляр сервиса
docker_builder_service = DockerBuilderService()