Исправление синтаксической ошибки в molecule_executor.py и обновление k8s preset'ов

- Исправлена незакрытая скобка в _build_test_command (строка 745)
- Добавлена поддержка k8s preset'ов: выполнение create_k8s_cluster.py перед create.yml
- Обновлены образы в k8s preset'ах: заменен недоступный ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy на inecs/ansible-lab:ubuntu22-latest
- Обновлены preset'ы в базе данных через SQL
- Обновлены файлы: k8s-single.yml, k8s-multi.yml, k8s-istio-full.yml
This commit is contained in:
Сергей Антропов
2026-02-16 00:31:09 +03:00
parent 1fbf9185a2
commit d4b0d6f848
26 changed files with 1913 additions and 646 deletions

View File

@@ -313,11 +313,24 @@ async def delete_dockerfile(
db: AsyncSession = Depends(get_async_db)
):
"""Удаление Dockerfile"""
# Получаем имя Dockerfile до удаления
dockerfile = await DockerfileService.get_dockerfile(db, dockerfile_id)
if not dockerfile:
raise HTTPException(status_code=404, detail="Dockerfile не найден")
dockerfile_name = dockerfile.name
# Удаляем Dockerfile
deleted = await DockerfileService.delete_dockerfile(db, dockerfile_id)
if not deleted:
raise HTTPException(status_code=404, detail="Dockerfile не найден")
return {"message": "Dockerfile удален успешно"}
return JSONResponse(content={
"success": True,
"dockerfile_id": dockerfile_id,
"dockerfile_name": dockerfile_name,
"message": f"Dockerfile '{dockerfile_name}' успешно удален"
})
@router.get("/api/v1/dockerfiles")

View File

@@ -206,11 +206,24 @@ async def delete_playbook(
db: AsyncSession = Depends(get_async_db)
):
"""Удаление playbook"""
# Получаем имя playbook до удаления
playbook = await PlaybookService.get_playbook(db, playbook_id)
if not playbook:
raise HTTPException(status_code=404, detail="Playbook не найден")
playbook_name = playbook.name
# Удаляем playbook
deleted = await PlaybookService.delete_playbook(db, playbook_id)
if not deleted:
raise HTTPException(status_code=404, detail="Playbook не найден")
return {"message": "Playbook удален успешно"}
return JSONResponse(content={
"success": True,
"playbook_id": playbook_id,
"playbook_name": playbook_name,
"message": f"Playbook '{playbook_name}' успешно удален"
})
@router.get("/api/v1/playbooks")

View File

@@ -11,10 +11,14 @@ from pathlib import Path
from typing import List, Dict, Optional
import yaml
import json
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.services.preset_service import PresetService
from app.db.session import get_async_db
from app.auth.deps import get_current_user
logger = logging.getLogger(__name__)
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
@@ -75,11 +79,34 @@ async def get_presets_api(
@router.get("/presets/create", response_class=HTMLResponse)
async def create_preset_page(request: Request):
async def create_preset_page(
request: Request,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Страница создания preset'а"""
from app.services.dockerfile_service import DockerfileService
# Загружаем список Dockerfiles из БД
dockerfiles = await DockerfileService.list_dockerfiles(db, status="active")
dockerfiles_list = [
{
"id": d.id,
"name": d.name,
"description": d.description,
"base_image": d.base_image,
"tags": d.tags,
"status": d.status
}
for d in dockerfiles
]
return templates.TemplateResponse(
"pages/presets/create.html",
{"request": request}
{
"request": request,
"dockerfiles": dockerfiles_list
}
)
@@ -129,6 +156,8 @@ async def create_preset_api(
description: str = Form(""),
category: str = Form("main"),
hosts: str = Form(""),
images: str = Form(""),
systemd_defaults: str = Form(""),
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для создания preset'а"""
@@ -137,12 +166,22 @@ async def create_preset_api(
if hosts:
hosts_list = json.loads(hosts)
images_dict = {}
if images:
images_dict = json.loads(images)
systemd_defaults_dict = {}
if systemd_defaults:
systemd_defaults_dict = json.loads(systemd_defaults)
preset = await PresetService.create_preset(
db=db,
preset_name=preset_name,
description=description,
hosts=hosts_list,
category=category
category=category,
images=images_dict,
systemd_defaults=systemd_defaults_dict
)
return JSONResponse(content={
@@ -270,6 +309,19 @@ async def preset_test_websocket(websocket: WebSocket, preset_name: str, category
"""WebSocket для live логов тестирования preset'а"""
await websocket.accept()
# Используем класс для хранения ссылки на контейнер
class ContainerRef:
def __init__(self):
self.container = None
def set(self, container):
self.container = container
def get(self):
return self.container
container_ref = ContainerRef()
executor = None
stop_requested = False
try:
# Получаем preset из БД
async for db in get_async_db():
@@ -285,18 +337,6 @@ async def preset_test_websocket(websocket: WebSocket, preset_name: str, category
preset_content = preset.content
break
# Получаем действие от клиента
data = await websocket.receive_json()
action = data.get("action", "start")
if action == "stop":
await websocket.send_json({
"type": "info",
"data": "⏹️ Остановка тестирования..."
})
await websocket.close()
return
# Запуск тестирования preset'а
from app.core.molecule_executor import MoleculeExecutor
executor = MoleculeExecutor()
@@ -309,34 +349,99 @@ async def preset_test_websocket(websocket: WebSocket, preset_name: str, category
"data": f"🚀 Запуск тестирования preset'а '{preset_name}'..."
})
# Запускаем тест (без указания роли - тестируем все роли)
async for line in executor.test_role(
role_name=None,
preset_name=preset_name,
preset_content=preset_content,
preset_category=category,
stream=True
):
line = line.rstrip()
if not line:
continue
log_type = executor.detect_log_level(line)
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
# Создаем задачу для мониторинга сообщений от клиента (стоп)
import asyncio
async def monitor_stop():
nonlocal stop_requested
try:
while True:
try:
data = await asyncio.wait_for(websocket.receive_json(), timeout=1.0)
action = data.get("action")
if action == "stop":
stop_requested = True
cont = container_ref.get()
if cont:
try:
cont.stop()
await websocket.send_json({
"type": "info",
"data": "⏹️ Остановка контейнера..."
})
except Exception as e:
logger.error(f"Error stopping container: {e}")
break
except asyncio.TimeoutError:
continue
except WebSocketDisconnect:
stop_requested = True
break
except Exception:
pass
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Тестирование preset'а завершено"
})
monitor_task = asyncio.create_task(monitor_stop())
# Запускаем тест (без указания роли - тестируем все роли)
try:
async for line in executor.test_role(
role_name=None,
preset_name=preset_name,
preset_content=preset_content,
preset_category=category,
stream=True,
stop_event=lambda: stop_requested,
container_ref=container_ref
):
if stop_requested:
break
line = line.rstrip()
if not line:
continue
log_type = executor.detect_log_level(line)
try:
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
except (WebSocketDisconnect, Exception) as e:
# Соединение закрыто - не пытаемся больше отправлять
stop_requested = True
logger.debug(f"WebSocket closed during log send: {e}")
break
# Отправляем финальное сообщение только если соединение открыто
try:
if not stop_requested:
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Тестирование preset'а завершено"
})
else:
await websocket.send_json({
"type": "complete",
"status": "stopped",
"data": "⏹️ Тестирование остановлено пользователем"
})
except (WebSocketDisconnect, Exception):
# Соединение уже закрыто - это нормально
pass
except GeneratorExit:
# Генератор закрыт, это нормально при закрытии WebSocket
stop_requested = True
finally:
monitor_task.cancel()
try:
await monitor_task
except asyncio.CancelledError:
pass
# Удаляем временный файл
if preset_name in executor._temp_preset_files:
if executor and preset_name in executor._temp_preset_files:
try:
executor._temp_preset_files[preset_name].unlink()
del executor._temp_preset_files[preset_name]
@@ -344,14 +449,22 @@ async def preset_test_websocket(websocket: WebSocket, preset_name: str, category
pass
except WebSocketDisconnect:
# Соединение закрыто клиентом - это нормально
pass
except GeneratorExit:
# Генератор закрыт - это нормально
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
})
error_msg = f"❌ Ошибка: {str(e)}"
try:
await websocket.send_json({
"type": "error",
"data": error_msg
})
except:
pass
logger.error(f"Error in preset_test_websocket: {e}\n{traceback.format_exc()}")
finally:
try:
await websocket.close()