Files
DevOpsLab/app/main.py
Сергей Антропов 1fbf9185a2 feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
2026-02-15 22:59:02 +03:00

319 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Главный файл FastAPI приложения
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse
from pathlib import Path
import uvicorn
import asyncio
import traceback
from app.core.config import settings
from app.api.v1.router import api_router
from app.db.session import init_db
from app.auth.middleware import AuthMiddleware
import logging
logger = logging.getLogger(__name__)
# Инициализация базы данных
init_db()
# Создание приложения
app = FastAPI(
title="DevOpsLab Web Interface",
description="Веб-интерфейс для управления Ansible ролями",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc"
)
# Добавление middleware для аутентификации
app.add_middleware(AuthMiddleware)
# Запуск миграций при старте (только для PostgreSQL)
@app.on_event("startup")
async def run_migrations_on_startup():
"""Запуск миграций Alembic при старте приложения"""
if settings.DATABASE_URL.startswith("sqlite"):
return
import asyncio
# Ждем, пока PostgreSQL будет готов
max_retries = 30
for i in range(max_retries):
try:
from alembic.config import Config
from alembic import command
alembic_cfg = Config()
# Устанавливаем путь к скриптам миграций
alembic_cfg.set_main_option("script_location", str(Path(__file__).parent / "alembic"))
# Устанавливаем URL базы данных
db_url = str(settings.DATABASE_URL).replace("postgresql+asyncpg://", "postgresql://")
alembic_cfg.set_main_option("sqlalchemy.url", db_url)
# Применяем миграции
command.upgrade(alembic_cfg, "head")
logger.info("Миграции применены успешно")
# Создаем пользователя admin, если его нет
from app.db.session import get_async_db
from app.services.user_service import UserService
async for db in get_async_db():
await UserService.ensure_admin_user(db)
break
return
except Exception as e:
if i < max_retries - 1:
logger.warning(f"Ожидание PostgreSQL... ({i+1}/{max_retries}): {e}")
await asyncio.sleep(2)
else:
logger.error(f"Ошибка при применении миграций: {e}")
# Подключение API роутера
app.include_router(api_router)
# Статические файлы
static_path = Path(__file__).parent / "static"
if static_path.exists():
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
# Шаблоны
templates_path = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
# Главная страница
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
"""Главная страница - Dashboard"""
return templates.TemplateResponse(
"pages/dashboard.html",
{"request": request}
)
# Роуты для ролей, тестов, preset'ов, деплоя, экспорта и импорта (подключаются через router)
# ВАЖНО: import должен быть ПЕРЕД roles, чтобы /roles/import не обрабатывался как /roles/{role_name}
from app.api.v1.endpoints import roles as roles_endpoints, tests as tests_endpoints, presets as presets_endpoints, deploy as deploy_endpoints, export as export_endpoints, auth as auth_endpoints, docker as docker_endpoints, vault as vault_endpoints, k8s as k8s_endpoints, playbooks as playbooks_endpoints, lint as lint_endpoints, profile as profile_endpoints
from app.api.v1.endpoints.import_role import router as import_endpoints
from app.api.v1.endpoints.dockerfiles_api import router as dockerfiles_endpoints
app.include_router(auth_endpoints.router) # Аутентификация
app.include_router(import_endpoints) # Первым, чтобы /roles/import работал
app.include_router(roles_endpoints.router)
app.include_router(tests_endpoints.router)
app.include_router(presets_endpoints.router)
app.include_router(deploy_endpoints.router)
app.include_router(lint_endpoints.router)
app.include_router(export_endpoints.router)
app.include_router(docker_endpoints.router)
app.include_router(vault_endpoints.router)
app.include_router(k8s_endpoints.router)
app.include_router(playbooks_endpoints.router)
app.include_router(dockerfiles_endpoints)
app.include_router(profile_endpoints.router)
# Health check
@app.get("/health")
async def health():
"""Проверка здоровья приложения"""
return {"status": "ok", "version": "1.0.0"}
# Обработчики ошибок
@app.exception_handler(404)
async def not_found_handler(request: Request, exc: HTTPException):
"""Обработчик 404 ошибки"""
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=404,
content={"detail": "Ресурс не найден"}
)
return templates.TemplateResponse(
"pages/errors/404.html",
{"request": request, "hide_sidebar": True},
status_code=404
)
@app.exception_handler(403)
async def forbidden_handler(request: Request, exc: HTTPException):
"""Обработчик 403 ошибки"""
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=403,
content={"detail": "Доступ запрещен"}
)
return templates.TemplateResponse(
"pages/errors/403.html",
{"request": request, "hide_sidebar": True},
status_code=403
)
@app.exception_handler(401)
async def unauthorized_handler(request: Request, exc: HTTPException):
"""Обработчик 401 ошибки"""
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=401,
content={"detail": "Требуется авторизация"}
)
return templates.TemplateResponse(
"pages/errors/401.html",
{"request": request, "hide_sidebar": True},
status_code=401
)
@app.exception_handler(500)
async def internal_server_error_handler(request: Request, exc: Exception):
"""Обработчик 500 ошибки"""
error_detail = None
# Показываем детали ошибки в режиме разработки или если включен DEBUG
if settings.DEBUG or settings.API_RELOAD:
error_detail = traceback.format_exc()
logger.error(f"Internal server error: {exc}", exc_info=True)
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=500,
content={
"detail": "Внутренняя ошибка сервера",
"error": str(exc) if (settings.DEBUG or settings.API_RELOAD) else None
}
)
return templates.TemplateResponse(
"pages/errors/500.html",
{
"request": request,
"error_detail": error_detail,
"hide_sidebar": True
},
status_code=500
)
@app.exception_handler(502)
async def bad_gateway_handler(request: Request, exc: Exception):
"""Обработчик 502 ошибки"""
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=502,
content={"detail": "Ошибка шлюза"}
)
return templates.TemplateResponse(
"pages/errors/502.html",
{"request": request, "hide_sidebar": True},
status_code=502
)
@app.exception_handler(503)
async def service_unavailable_handler(request: Request, exc: Exception):
"""Обработчик 503 ошибки"""
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=503,
content={"detail": "Сервис недоступен"}
)
return templates.TemplateResponse(
"pages/errors/503.html",
{"request": request, "hide_sidebar": True},
status_code=503
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""Обработчик общих HTTP исключений"""
status_code = exc.status_code
# Обрабатываем уже определенные коды ошибок
if status_code == 404:
return await not_found_handler(request, exc)
elif status_code == 403:
return await forbidden_handler(request, exc)
elif status_code == 401:
return await unauthorized_handler(request, exc)
# Для остальных кодов используем общий шаблон
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=status_code,
content={"detail": exc.detail}
)
error_messages = {
400: ("400", "Неверный запрос", "Запрос содержит некорректные данные."),
405: ("405", "Метод не разрешен", "Используемый HTTP метод не поддерживается для этого ресурса."),
408: ("408", "Истекло время ожидания", "Запрос занял слишком много времени."),
429: ("429", "Слишком много запросов", "Превышен лимит запросов. Попробуйте позже."),
}
error_code, error_title, error_description = error_messages.get(
status_code,
(str(status_code), "Ошибка", exc.detail or "Произошла ошибка при обработке запроса.")
)
return templates.TemplateResponse(
"pages/errors/error.html",
{
"request": request,
"error_code": error_code,
"error_title": error_title,
"error_message": exc.detail,
"error_description": error_description,
"hide_sidebar": True
},
status_code=status_code
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""Обработчик всех необработанных исключений"""
error_detail = None
# Показываем детали ошибки в режиме разработки или если включен DEBUG
if settings.DEBUG or settings.API_RELOAD:
error_detail = traceback.format_exc()
logger.error(f"Unhandled exception: {exc}", exc_info=True)
if request.url.path.startswith("/api/"):
return JSONResponse(
status_code=500,
content={
"detail": "Внутренняя ошибка сервера",
"error": str(exc) if (settings.DEBUG or settings.API_RELOAD) else None
}
)
return templates.TemplateResponse(
"pages/errors/500.html",
{
"request": request,
"error_detail": error_detail,
"hide_sidebar": True
},
status_code=500
)
if __name__ == "__main__":
uvicorn.run(
"app.main:app",
host=settings.API_HOST,
port=settings.API_PORT,
reload=settings.API_RELOAD,
workers=settings.API_WORKERS
)