"""Веб-интерфейс и REST API для управления локальными кластерами kind. В контейнере uvicorn слушает порт 6000; на хост публикация по умолчанию 8080 (``KIND_K8S_WEB_PORT``), т.к. 6000 на хосте блокируется Chrome (ERR_UNSAFE_PORT). Запуск в контейнере: ``python3 -m uvicorn main:app --host 0.0.0.0 --port 6000`` из каталога ``/opt/kind-k8s/app`` или через ``make docker up`` / ``make podman up``. Автор: Сергей Антропов Сайт: https://devops.org.ru """ from __future__ import annotations import logging import os from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from api.v1.router import api_router from core.config import get_settings _BASE = Path(__file__).resolve().parent logger = logging.getLogger("kind_k8s.web") def _configure_logging() -> None: """Единая настройка логов для uvicorn и модулей kind-k8s.""" if logging.root.handlers: return level = logging.DEBUG if os.environ.get("KIND_K8S_DEBUG", "").strip().lower() in ("1", "true", "yes", "да") else logging.INFO logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s") logger.info("Логирование инициализировано, уровень=%s", logging.getLevelName(level)) @asynccontextmanager async def lifespan(app: FastAPI): """Старт и остановка приложения.""" _configure_logging() settings = get_settings() logger.info("Запуск FastAPI «%s»", settings.app_title) yield logger.info("Остановка FastAPI") settings = get_settings() app = FastAPI(title=settings.app_title, lifespan=lifespan) app.include_router(api_router, prefix="/api/v1") _templates_dir = _BASE / "templates" _static_dir = _BASE / "static" if _static_dir.is_dir(): app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static") templates = Jinja2Templates(directory=str(_templates_dir)) @app.get("/", response_class=HTMLResponse, summary="Веб-интерфейс") async def dashboard(request: Request) -> HTMLResponse: """Главная страница: обзор, сводка по узлам, таблица кластеров.""" if not _templates_dir.is_dir(): return HTMLResponse( content="

Шаблоны не найдены. Ожидается каталог app/templates/

", status_code=500, ) # Starlette ≥0.37: первым аргументом обязателен Request (иначе dict уйдёт в get_template → unhashable type). return templates.TemplateResponse( request, "dashboard.html", {"app_title": settings.app_title, "nav_active": "panel"}, ) @app.get("/cluster-create", response_class=HTMLResponse, summary="Создание кластера и задания") async def cluster_create_page(request: Request) -> HTMLResponse: """Форма создания кластера, прогресс и список последних заданий.""" if not _templates_dir.is_dir(): return HTMLResponse( content="

Шаблоны не найдены. Ожидается каталог app/templates/

", status_code=500, ) return templates.TemplateResponse( request, "cluster_create.html", {"app_title": settings.app_title, "nav_active": "cluster_create"}, ) @app.get("/ui", include_in_schema=False) async def ui_redirect() -> RedirectResponse: """Удобный алиас на корень UI.""" return RedirectResponse(url="/", status_code=307) @app.get("/favicon.ico", include_in_schema=False) async def favicon_ico() -> FileResponse: """Браузеры по умолчанию запрашивают ``/favicon.ico``; отдаём SVG в стиле логотипа Kubernetes.""" path = _static_dir / "favicon.svg" if not path.is_file(): raise HTTPException(status_code=404, detail="favicon не найден") return FileResponse(path, media_type="image/svg+xml", filename="favicon.ico") @app.get("/documentation", response_class=HTMLResponse, summary="Документация (README)") async def documentation_page(request: Request) -> HTMLResponse: """Оболочка страницы: Markdown подгружается с ``GET /api/v1/docs/readme``, рендер в браузере (marked + DOMPurify из ``/static/js/vendor/``).""" if not _templates_dir.is_dir(): return HTMLResponse( content="

Шаблоны не найдены.

", status_code=500, ) return templates.TemplateResponse( request, "documentation.html", {"app_title": settings.app_title, "nav_active": "documentation"}, )