feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

169
app/core/make_executor.py Normal file
View File

@@ -0,0 +1,169 @@
"""
Выполнение Makefile команд
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import subprocess
import asyncio
from typing import Dict, List, Optional, AsyncGenerator
from pathlib import Path
from app.core.config import settings
from datetime import datetime
import json
import re
class MakeExecutor:
"""Выполнение Makefile команд с отслеживанием прогресса"""
def __init__(self):
self.project_root = settings.PROJECT_ROOT
def detect_command_type(self, command: str) -> str:
"""Определение типа команды"""
if "test" in command:
return "test"
elif "deploy" in command:
return "deploy"
elif "export" in command:
return "export"
elif "import" in command:
return "import"
elif "lint" in command:
return "lint"
else:
return "other"
def parse_parameters(self, command: str, args: List[str] = None) -> dict:
"""Парсинг параметров команды"""
params = {}
# Парсинг команды
parts = command.split()
if len(parts) > 2:
params["role_name"] = parts[2]
if len(parts) > 3:
params["preset"] = parts[3]
# Парсинг аргументов
if args:
for i, arg in enumerate(args):
if arg.startswith("--"):
key = arg[2:].replace("-", "_")
if i + 1 < len(args) and not args[i + 1].startswith("--"):
params[key] = args[i + 1]
else:
params[key] = True
return params
async def execute(
self,
command: str,
args: List[str] = None,
user: str = None
) -> Dict:
"""
Выполнение команды make
Args:
command: Команда make (например, "role test nginx default")
args: Дополнительные аргументы
user: Пользователь, выполнивший команду
Returns:
Словарь с результатами выполнения
"""
cmd = ["make"] + command.split() + (args or [])
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=self.project_root,
timeout=3600 # 1 час максимум
)
return {
"success": result.returncode == 0,
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode,
"command": " ".join(cmd),
"timestamp": datetime.now().isoformat()
}
except subprocess.TimeoutExpired:
return {
"success": False,
"stdout": "",
"stderr": "Command timeout after 1 hour",
"returncode": -1,
"command": " ".join(cmd),
"timestamp": datetime.now().isoformat()
}
except Exception as e:
return {
"success": False,
"stdout": "",
"stderr": str(e),
"returncode": -1,
"command": " ".join(cmd),
"timestamp": datetime.now().isoformat()
}
async def execute_stream(
self,
command: str,
args: List[str] = None
) -> AsyncGenerator[str, None]:
"""
Выполнение команды с потоковым выводом для WebSocket
Args:
command: Команда make
args: Дополнительные аргументы
Yields:
Строки вывода команды
"""
cmd = ["make"] + command.split() + (args or [])
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
cwd=str(self.project_root)
)
async for line in process.stdout:
yield line.decode('utf-8', errors='replace')
await process.wait()
def detect_log_level(self, line: str) -> str:
"""Определение уровня лога из строки"""
line_lower = line.lower()
if any(word in line_lower for word in ["error", "failed", "fatal"]):
return "error"
elif any(word in line_lower for word in ["warning", "warn"]):
return "warning"
elif any(word in line_lower for word in ["changed", "ok"]):
return "info"
else:
return "debug"
def parse_play_recap(self, line: str) -> dict:
"""Парсинг PLAY RECAP из Ansible вывода"""
# Пример: "web1.example.com : ok=5 changed=3 unreachable=0 failed=0"
match = re.search(r'(\S+)\s*:\s*ok=(\d+)\s+changed=(\d+).*failed=(\d+)', line)
if match:
return {
"host": match.group(1),
"ok": int(match.group(2)),
"changed": int(match.group(3)),
"failed": int(match.group(4)),
"status": "success" if int(match.group(4)) == 0 else "failed"
}
return {}