feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

View 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()