- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
274 lines
9.5 KiB
Python
274 lines
9.5 KiB
Python
"""
|
||
API endpoints для тестирования ролей
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
|
||
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, Depends
|
||
from fastapi.responses import HTMLResponse
|
||
from fastapi.templating import Jinja2Templates
|
||
from pathlib import Path
|
||
from typing import Dict, Optional
|
||
import json
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from app.core.config import settings
|
||
from app.core.molecule_executor import MoleculeExecutor
|
||
from app.services.preset_service import PresetService
|
||
from app.db.session import get_async_db
|
||
|
||
router = APIRouter()
|
||
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
|
||
templates = Jinja2Templates(directory=str(templates_path))
|
||
molecule_executor = MoleculeExecutor()
|
||
|
||
|
||
@router.get("/roles/{role_name}/test", response_class=HTMLResponse)
|
||
async def test_role_page(
|
||
request: Request,
|
||
role_name: str,
|
||
db: AsyncSession = Depends(get_async_db)
|
||
):
|
||
"""Страница тестирования роли"""
|
||
# Проверка существования роли
|
||
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
|
||
if not roles_dir.exists():
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
|
||
|
||
# Получение списка preset'ов из БД
|
||
presets = await PresetService.get_all_presets(db)
|
||
|
||
return templates.TemplateResponse(
|
||
"pages/roles/test.html",
|
||
{
|
||
"request": request,
|
||
"role_name": role_name,
|
||
"presets": presets
|
||
}
|
||
)
|
||
|
||
|
||
@router.post("/api/v1/roles/{role_name}/test")
|
||
async def start_role_test(
|
||
role_name: str,
|
||
preset: str = "default",
|
||
variables: Optional[str] = None
|
||
):
|
||
"""Запуск теста роли"""
|
||
# TODO: Запуск через Celery для фонового выполнения
|
||
# Пока просто возвращаем информацию
|
||
return {
|
||
"success": True,
|
||
"role_name": role_name,
|
||
"preset": preset,
|
||
"message": "Тест запущен",
|
||
"test_id": f"{role_name}-{preset}"
|
||
}
|
||
|
||
|
||
@router.get("/tests", response_class=HTMLResponse)
|
||
async def tests_history_page(
|
||
request: Request,
|
||
role_name: Optional[str] = None,
|
||
page: int = 1,
|
||
per_page: int = 20,
|
||
db: AsyncSession = Depends(get_async_db)
|
||
):
|
||
"""Страница истории тестов"""
|
||
try:
|
||
from app.services.history_service import HistoryService
|
||
from sqlalchemy import select, func
|
||
from app.models.database import CommandHistory, TestResult
|
||
|
||
history_service = HistoryService()
|
||
|
||
# Получение тестов из БД
|
||
query = select(CommandHistory).where(
|
||
CommandHistory.command_type == "test"
|
||
)
|
||
|
||
if role_name:
|
||
query = query.where(CommandHistory.command.like(f"%{role_name}%"))
|
||
|
||
query = query.order_by(CommandHistory.created_at.desc())
|
||
|
||
# Подсчет общего количества
|
||
count_query = select(func.count(CommandHistory.id)).where(
|
||
CommandHistory.command_type == "test"
|
||
)
|
||
if role_name:
|
||
count_query = count_query.where(CommandHistory.command.like(f"%{role_name}%"))
|
||
|
||
total = (await db.execute(count_query)).scalar() or 0
|
||
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
|
||
page = max(1, min(page, total_pages))
|
||
|
||
# Пагинация
|
||
offset = (page - 1) * per_page
|
||
query = query.offset(offset).limit(per_page)
|
||
|
||
result = await db.execute(query)
|
||
tests = result.scalars().all()
|
||
|
||
# Получение списка ролей для фильтра
|
||
roles_dir = settings.PROJECT_ROOT / "roles"
|
||
roles = []
|
||
if roles_dir.exists():
|
||
for role_dir in roles_dir.iterdir():
|
||
if role_dir.is_dir() and role_dir.name != "deploy.yml":
|
||
roles.append(role_dir.name)
|
||
|
||
return templates.TemplateResponse(
|
||
"pages/tests/index.html",
|
||
{
|
||
"request": request,
|
||
"tests": tests,
|
||
"roles": sorted(roles),
|
||
"role_name": role_name or "",
|
||
"total": total,
|
||
"page": page,
|
||
"per_page": per_page,
|
||
"total_pages": total_pages
|
||
}
|
||
)
|
||
except Exception as e:
|
||
import logging
|
||
logging.error(f"Error loading tests history: {e}", exc_info=True)
|
||
# Если база данных не настроена, показываем пустую страницу
|
||
return templates.TemplateResponse(
|
||
"pages/tests/index.html",
|
||
{
|
||
"request": request,
|
||
"tests": [],
|
||
"roles": [],
|
||
"role_name": role_name or "",
|
||
"total": 0,
|
||
"page": 1,
|
||
"per_page": per_page,
|
||
"total_pages": 1,
|
||
"error": "База данных не настроена или недоступна"
|
||
}
|
||
)
|
||
|
||
|
||
@router.get("/api/v1/tests/recent")
|
||
async def get_recent_tests(limit: int = 10):
|
||
"""Получение последних тестов"""
|
||
try:
|
||
from app.services.history_service import HistoryService
|
||
history_service = HistoryService()
|
||
tests = history_service.get_command_history(
|
||
limit=limit,
|
||
command_type="test"
|
||
)
|
||
return tests
|
||
except Exception:
|
||
# Если база данных не настроена, возвращаем пустой список
|
||
return []
|
||
|
||
|
||
@router.websocket("/ws/test/{test_id}")
|
||
async def test_websocket(websocket: WebSocket, test_id: str):
|
||
"""WebSocket для live логов тестирования"""
|
||
await websocket.accept()
|
||
|
||
try:
|
||
# Парсинг test_id (формат: role_name-preset или role_name-preset-category)
|
||
parts = test_id.rsplit("-", 2)
|
||
if len(parts) < 2:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": "Неверный формат test_id. Ожидается: role_name-preset или role_name-preset-category"
|
||
})
|
||
await websocket.close()
|
||
return
|
||
|
||
role_name = parts[0]
|
||
preset_name = parts[1] if len(parts) > 1 else "default"
|
||
preset_category = parts[2] if len(parts) > 2 else "main"
|
||
|
||
# Проверка существования роли
|
||
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
|
||
if not roles_dir.exists():
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": f"Роль '{role_name}' не найдена"
|
||
})
|
||
await websocket.close()
|
||
return
|
||
|
||
# Получаем preset из БД
|
||
async for db in get_async_db():
|
||
preset = await PresetService.get_preset(db, preset_name, preset_category)
|
||
if not preset:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": f"Preset '{preset_name}' не найден"
|
||
})
|
||
await websocket.close()
|
||
return
|
||
|
||
preset_content = preset.content
|
||
break
|
||
|
||
# Отправка начального сообщения
|
||
await websocket.send_json({
|
||
"type": "info",
|
||
"data": f"🚀 Запуск теста роли '{role_name}' с preset '{preset_name}'..."
|
||
})
|
||
|
||
# Создаем временный файл preset'а из БД
|
||
molecule_executor.create_temp_preset_file(preset_name, preset_content, preset_category)
|
||
|
||
# Запуск теста через MoleculeExecutor (без Makefile)
|
||
async for line in molecule_executor.test_role(
|
||
role_name=role_name,
|
||
preset_name=preset_name,
|
||
preset_content=preset_content,
|
||
preset_category=preset_category,
|
||
stream=True
|
||
):
|
||
# Очистка строки от лишних символов
|
||
line = line.rstrip()
|
||
if not line:
|
||
continue
|
||
|
||
# Определение типа лога
|
||
log_type = molecule_executor.detect_log_level(line)
|
||
|
||
await websocket.send_json({
|
||
"type": "log",
|
||
"level": log_type,
|
||
"data": line
|
||
})
|
||
|
||
# Завершение
|
||
await websocket.send_json({
|
||
"type": "complete",
|
||
"status": "success",
|
||
"data": "✅ Тест завершен успешно"
|
||
})
|
||
|
||
# Удаляем временный файл
|
||
if preset_name in molecule_executor._temp_preset_files:
|
||
try:
|
||
molecule_executor._temp_preset_files[preset_name].unlink()
|
||
del molecule_executor._temp_preset_files[preset_name]
|
||
except:
|
||
pass
|
||
|
||
except WebSocketDisconnect:
|
||
pass
|
||
except Exception as e:
|
||
import traceback
|
||
error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}"
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": error_msg
|
||
})
|
||
finally:
|
||
try:
|
||
await websocket.close()
|
||
except:
|
||
pass
|