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