- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
237 lines
9.7 KiB
Python
237 lines
9.7 KiB
Python
"""
|
||
Сервис для взаимодействия с 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()
|