feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
254
app/api/v1/endpoints/deploy.py
Normal file
254
app/api/v1/endpoints/deploy.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
API endpoints для деплоя на живые серверы
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, HTTPException
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
import yaml
|
||||
import json
|
||||
from app.core.config import settings
|
||||
from app.services.deployment_service import DeploymentService
|
||||
|
||||
router = APIRouter()
|
||||
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
|
||||
templates = Jinja2Templates(directory=str(templates_path))
|
||||
deployment_service = DeploymentService()
|
||||
|
||||
|
||||
@router.get("/deploy", response_class=HTMLResponse)
|
||||
async def deploy_page(request: Request):
|
||||
"""Страница деплоя"""
|
||||
# Проверка наличия inventory
|
||||
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
|
||||
inventory_exists = inventory_file.exists()
|
||||
|
||||
# Чтение inventory если существует
|
||||
inventory_content = ""
|
||||
inventory_data = {}
|
||||
if inventory_exists:
|
||||
try:
|
||||
inventory_content = inventory_file.read_text()
|
||||
# Парсинг inventory для отображения групп и хостов
|
||||
inventory_data = parse_inventory(inventory_content)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Получение списка ролей
|
||||
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/deploy/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"inventory_exists": inventory_exists,
|
||||
"inventory_content": inventory_content,
|
||||
"inventory_data": inventory_data,
|
||||
"roles": sorted(roles)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/deploy/inventory", response_class=HTMLResponse)
|
||||
async def inventory_page(request: Request):
|
||||
"""Страница управления inventory"""
|
||||
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
|
||||
inventory_exists = inventory_file.exists()
|
||||
|
||||
inventory_content = ""
|
||||
if inventory_exists:
|
||||
inventory_content = inventory_file.read_text()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"pages/deploy/inventory.html",
|
||||
{
|
||||
"request": request,
|
||||
"inventory_exists": inventory_exists,
|
||||
"inventory_content": inventory_content
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/deploy/inventory")
|
||||
async def save_inventory(request: Request):
|
||||
"""Сохранение inventory файла"""
|
||||
form_data = await request.form()
|
||||
content = form_data.get("content", "")
|
||||
|
||||
if not content:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "Содержимое inventory не может быть пустым"}
|
||||
)
|
||||
|
||||
inventory_dir = settings.PROJECT_ROOT / "inventory"
|
||||
inventory_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
inventory_file = inventory_dir / "hosts.ini"
|
||||
inventory_file.write_text(content)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": "Inventory файл успешно сохранен"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/deploy/start")
|
||||
async def start_deploy(
|
||||
role_name: Optional[str] = None,
|
||||
tags: Optional[str] = None,
|
||||
limit: Optional[str] = None,
|
||||
check: bool = False,
|
||||
extra_vars: Optional[str] = None
|
||||
):
|
||||
"""Запуск деплоя"""
|
||||
# TODO: Запуск через Celery для фонового выполнения
|
||||
# Пока просто возвращаем информацию
|
||||
deploy_id = f"deploy-{role_name or 'all'}-{tags or 'none'}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"deploy_id": deploy_id,
|
||||
"role_name": role_name,
|
||||
"tags": tags,
|
||||
"limit": limit,
|
||||
"check": check,
|
||||
"message": "Деплой запущен"
|
||||
}
|
||||
|
||||
|
||||
@router.websocket("/ws/deploy/{deploy_id}")
|
||||
async def deploy_websocket(websocket: WebSocket, deploy_id: str):
|
||||
"""WebSocket для live логов деплоя"""
|
||||
await websocket.accept()
|
||||
|
||||
try:
|
||||
# Ждем сообщение от клиента с параметрами деплоя
|
||||
message = await websocket.receive_json()
|
||||
|
||||
if message.get("type") != "start":
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"data": "Ожидается сообщение типа 'start' с параметрами деплоя"
|
||||
})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
# Получаем параметры из сообщения
|
||||
role_name = message.get("role_name")
|
||||
inventory = message.get("inventory", "inventory/hosts.ini")
|
||||
limit = message.get("limit")
|
||||
tags = message.get("tags")
|
||||
check = message.get("check", False)
|
||||
extra_vars = message.get("extra_vars")
|
||||
|
||||
if not role_name:
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"data": "Не указано имя роли"
|
||||
})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
# Отправка начального сообщения
|
||||
await websocket.send_json({
|
||||
"type": "info",
|
||||
"data": f"🚀 Запуск деплоя роли '{role_name}'..."
|
||||
})
|
||||
|
||||
if check:
|
||||
await websocket.send_json({
|
||||
"type": "warning",
|
||||
"data": "⚠️ Режим dry-run (--check) - изменения не будут применены"
|
||||
})
|
||||
|
||||
# Запуск деплоя через DeploymentService
|
||||
async for line in deployment_service.deploy_role(
|
||||
role_name=role_name,
|
||||
inventory=inventory,
|
||||
limit=limit,
|
||||
tags=tags,
|
||||
check=check,
|
||||
extra_vars=extra_vars,
|
||||
stream=True
|
||||
):
|
||||
line = line.rstrip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Определение типа лога
|
||||
log_type = deployment_service.detect_log_level(line)
|
||||
|
||||
await websocket.send_json({
|
||||
"type": "log",
|
||||
"level": log_type,
|
||||
"data": line
|
||||
})
|
||||
|
||||
# Завершение
|
||||
await websocket.send_json({
|
||||
"type": "complete",
|
||||
"status": "success",
|
||||
"data": "✅ Деплой завершен успешно"
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
|
||||
def parse_inventory(content: str) -> Dict:
|
||||
"""Парсинг inventory файла для отображения структуры"""
|
||||
result = {
|
||||
"groups": {},
|
||||
"hosts": []
|
||||
}
|
||||
|
||||
current_group = None
|
||||
for line in content.split("\n"):
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
# Группа
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
group_name = line[1:-1]
|
||||
if ":" in group_name:
|
||||
group_name = group_name.split(":")[0]
|
||||
current_group = group_name
|
||||
if current_group not in result["groups"]:
|
||||
result["groups"][current_group] = []
|
||||
# Хост
|
||||
elif current_group and " " in line:
|
||||
host_parts = line.split()
|
||||
host_name = host_parts[0]
|
||||
result["groups"][current_group].append(host_name)
|
||||
result["hosts"].append({
|
||||
"name": host_name,
|
||||
"group": current_group,
|
||||
"vars": " ".join(host_parts[1:]) if len(host_parts) > 1 else ""
|
||||
})
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user