- Цель make docker|podman kubectl CLUSTER=… (KUBECTL_ARGS) — exec kubectl в kind-k8s-web - README: без kubectl на хосте; раздел про проверку API из контейнера - create_cluster/cluster_status: подсказки для UI, make kubectl и exec в контейнере - app/docs: api_routes.md и README.md про kubectl и API workloads - Прочее: переименование проекта, документация, UI документации (ранее в рабочем дереве)
130 lines
4.9 KiB
Python
130 lines
4.9 KiB
Python
"""Чтение README.md и безопасное чтение ``app/docs/*.md`` для API документации.
|
||
|
||
Разметка Markdown преобразуется в браузере: ``/static/js/vendor/marked.min.js`` и
|
||
``purify.min.js`` (файлы входят в репозиторий, без CDN).
|
||
|
||
README: ``KIND_K8S_README_PATH`` или ``README.md`` в корне рядом с ``app/``;
|
||
в Docker-образе — ``/opt/kind-k8s/README.md``. Файлы под ``app/docs/`` — через
|
||
``resolve_app_docs_markdown`` / ``read_app_docs_file_text`` (эндпоинт ``GET /api/v1/docs/file``).
|
||
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
from pathlib import Path
|
||
|
||
logger = logging.getLogger("kind_k8s.readme_doc")
|
||
|
||
# app/core/readme_doc.py: parents[2] = корень репозитория (рядом с app/) или /opt/kind-k8s в образе
|
||
_LIB_FILE = Path(__file__).resolve()
|
||
|
||
|
||
def _candidates_without_env() -> list[Path]:
|
||
"""
|
||
Возможные пути к README без KIND_K8S_README_PATH.
|
||
|
||
Порядок: родитель каталога app/ (типично репозиторий), затем фиксированный путь образа.
|
||
В compose рекомендуется монтировать ./README.md → /opt/kind-k8s/README.md (см. docker-compose.yml).
|
||
"""
|
||
out: list[Path] = []
|
||
seen: set[Path] = set()
|
||
try:
|
||
repo_readme = (_LIB_FILE.parents[2] / "README.md").resolve()
|
||
if repo_readme not in seen:
|
||
seen.add(repo_readme)
|
||
out.append(repo_readme)
|
||
except (IndexError, OSError):
|
||
pass
|
||
fixed = Path("/opt/kind-k8s/README.md")
|
||
try:
|
||
fixed_r = fixed.resolve()
|
||
if fixed_r not in seen:
|
||
seen.add(fixed_r)
|
||
out.append(fixed_r)
|
||
except OSError:
|
||
out.append(fixed)
|
||
return out
|
||
|
||
|
||
def get_readme_path() -> Path | None:
|
||
"""Первый существующий путь к README или ``None``."""
|
||
raw = (os.environ.get("KIND_K8S_README_PATH") or "").strip()
|
||
if raw:
|
||
p = Path(raw).expanduser().resolve()
|
||
return p if p.is_file() else None
|
||
for p in _candidates_without_env():
|
||
if p.is_file():
|
||
return p
|
||
return None
|
||
|
||
|
||
def read_readme_text() -> str:
|
||
"""Прочитать README как UTF-8; ``FileNotFoundError`` если файла нет."""
|
||
raw = (os.environ.get("KIND_K8S_README_PATH") or "").strip()
|
||
if raw:
|
||
p = Path(raw).expanduser().resolve()
|
||
if not p.is_file():
|
||
logger.warning("KIND_K8S_README_PATH: файл не найден: %s", p)
|
||
raise FileNotFoundError(str(p))
|
||
text = p.read_text(encoding="utf-8")
|
||
logger.debug("README из KIND_K8S_README_PATH, %s символов", len(text))
|
||
return text
|
||
|
||
for p in _candidates_without_env():
|
||
if p.is_file():
|
||
text = p.read_text(encoding="utf-8")
|
||
logger.info("README прочитан: %s (%s символов)", p, len(text))
|
||
return text
|
||
|
||
logger.warning(
|
||
"README.md не найден. Проверены пути: %s. "
|
||
"В Docker Compose добавьте монтирование ./README.md:/opt/kind-k8s/README.md "
|
||
"или пересоберите образ (COPY README.md в Dockerfile).",
|
||
[str(x) for x in _candidates_without_env()],
|
||
)
|
||
raise FileNotFoundError("README.md")
|
||
|
||
|
||
def repo_root() -> Path:
|
||
"""Корень репозитория (родитель каталога ``app/``)."""
|
||
return _LIB_FILE.parents[2]
|
||
|
||
|
||
def resolve_app_docs_markdown(relative_path: str) -> Path | None:
|
||
"""
|
||
Безопасно разрешить путь к ``.md`` только внутри ``app/docs/``.
|
||
|
||
Ожидается вид ``app/docs/api_routes.md`` (без ``..`` и абсолютных путей).
|
||
"""
|
||
raw = (relative_path or "").strip().lstrip("/").replace("\\", "/")
|
||
if not raw or ".." in raw or "\x00" in raw:
|
||
return None
|
||
if not raw.endswith(".md"):
|
||
return None
|
||
if not raw.startswith("app/docs/"):
|
||
return None
|
||
base = repo_root()
|
||
try:
|
||
full = (base / raw).resolve()
|
||
allowed = (base / "app" / "docs").resolve()
|
||
full.relative_to(allowed)
|
||
except (ValueError, OSError):
|
||
return None
|
||
if not full.is_file():
|
||
return None
|
||
return full
|
||
|
||
|
||
def read_app_docs_file_text(relative_path: str) -> str:
|
||
"""Прочитать UTF-8 текст файла под ``app/docs/``; ``FileNotFoundError`` если путь недопустим или файла нет."""
|
||
p = resolve_app_docs_markdown(relative_path)
|
||
if p is None:
|
||
raise FileNotFoundError(relative_path)
|
||
text = p.read_text(encoding="utf-8")
|
||
logger.debug("Документ app/docs: %s, %s символов", p, len(text))
|
||
return text
|