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