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