""" Сервис для взаимодействия с 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()