- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
319 lines
12 KiB
Python
319 lines
12 KiB
Python
"""
|
||
Главный файл 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
|
||
)
|