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