Документация и kubectl из контейнера; Kind Clusters Dashboard
- Цель 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 документации (ранее в рабочем дереве)
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
# kind-k8s-develop — веб-интерфейс (FastAPI) для kind.
|
# Kind Clusters Dashboard — веб-интерфейс (FastAPI) для kind.
|
||||||
# Создание кластеров — в браузере: http://127.0.0.1:8080 (порт: KIND_K8S_WEB_PORT; 6000 на хосте — ERR_UNSAFE_PORT в Chrome).
|
# Создание кластеров — в браузере: http://127.0.0.1:8080 (порт: KIND_K8S_WEB_PORT; 6000 на хосте — ERR_UNSAFE_PORT в Chrome).
|
||||||
#
|
#
|
||||||
# Все операции с Compose только с явным выбором среды:
|
# Все операции с Compose только с явным выбором среды:
|
||||||
# make docker up | make docker down | make docker logs | …
|
# make docker up | make docker down | make docker logs | …
|
||||||
# make podman up | make podman down | …
|
# make podman up | make podman down | …
|
||||||
# make docker rebuild / make podman rebuild — образ без кэша и пересоздание контейнера
|
# make docker rebuild / make podman rebuild — образ без кэша и пересоздание контейнера
|
||||||
# Без префикса docker/podman цели up/down/logs/ps/compose-build/rebuild/check-docker завершатся с подсказкой.
|
# Без префикса docker/podman цели up/down/logs/ps/compose-build/rebuild/check-docker/kubectl завершатся с подсказкой.
|
||||||
#
|
#
|
||||||
# Автор: Сергей Антропов — https://devops.org.ru
|
# Автор: Сергей Антропов — https://devops.org.ru
|
||||||
|
|
||||||
@@ -15,13 +15,16 @@ else ifneq (,$(filter docker,$(MAKECMDGOALS)))
|
|||||||
COMPOSE := docker compose
|
COMPOSE := docker compose
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: help docker podman _require_runtime up down logs ps setup clusters-dir check-docker compose-build rebuild
|
.PHONY: help docker podman _require_runtime up down logs ps setup clusters-dir check-docker compose-build rebuild kubectl
|
||||||
|
|
||||||
KIND_K8S_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
KIND_K8S_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
||||||
SETUP_ENV_SCRIPT := $(KIND_K8S_DIR)/scripts/setup_env_interactive.py
|
SETUP_ENV_SCRIPT := $(KIND_K8S_DIR)/scripts/setup_env_interactive.py
|
||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
# При «exec format error» у kind: make docker compose-build COMPOSE_BUILD_FLAGS=--platform linux/arm64
|
# При «exec format error» у kind: make docker compose-build COMPOSE_BUILD_FLAGS=--platform linux/arm64
|
||||||
COMPOSE_BUILD_FLAGS ?=
|
COMPOSE_BUILD_FLAGS ?=
|
||||||
|
# Для цели kubectl: имя кластера и аргументы kubectl после --kubeconfig (по умолчанию: get nodes).
|
||||||
|
CLUSTER ?=
|
||||||
|
KUBECTL_ARGS ?= get nodes
|
||||||
|
|
||||||
help: ## Справка по целям
|
help: ## Справка по целям
|
||||||
@echo "Веб-UI kind — только с выбором Docker или Podman в одной команде с целью:"
|
@echo "Веб-UI kind — только с выбором Docker или Podman в одной команде с целью:"
|
||||||
@@ -32,6 +35,7 @@ help: ## Справка по целям
|
|||||||
@echo " make docker compose-build / make podman compose-build"
|
@echo " make docker compose-build / make podman compose-build"
|
||||||
@echo " make docker rebuild / make podman rebuild (build --no-cache + up --force-recreate)"
|
@echo " make docker rebuild / make podman rebuild (build --no-cache + up --force-recreate)"
|
||||||
@echo " make docker check-docker / make podman check-docker"
|
@echo " make docker check-docker / make podman check-docker"
|
||||||
|
@echo " make docker kubectl CLUSTER=<имя> — kubectl в контейнере (см. KUBECTL_ARGS, по умолчанию get nodes)"
|
||||||
@echo "Без установки Compose: make setup, make clusters-dir (python3 для setup)."
|
@echo "Без установки Compose: make setup, make clusters-dir (python3 для setup)."
|
||||||
@grep -E '^[a-zA-Z0-9_-]+:.*?##' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?##"} {printf " \033[36m%-28s\033[0m %s\n", $$1, $$2}'
|
@grep -E '^[a-zA-Z0-9_-]+:.*?##' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?##"} {printf " \033[36m%-28s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
@@ -41,12 +45,12 @@ docker: ## Маркер среды: задайте вторую цель (нап
|
|||||||
podman: ## Маркер среды: задайте вторую цель (например: make podman up)
|
podman: ## Маркер среды: задайте вторую цель (например: make podman up)
|
||||||
@:
|
@:
|
||||||
|
|
||||||
# Общая проверка: цели up/down/logs/ps/compose-build/rebuild/check-docker — только make docker … / make podman …
|
# Общая проверка: цели up/down/logs/ps/compose-build/rebuild/check-docker/kubectl — только make docker … / make podman …
|
||||||
_require_runtime:
|
_require_runtime:
|
||||||
@if [ -z "$(COMPOSE)" ]; then \
|
@if [ -z "$(COMPOSE)" ]; then \
|
||||||
echo >&2 "Укажите среду в той же команде, что и цель:"; \
|
echo >&2 "Укажите среду в той же команде, что и цель:"; \
|
||||||
echo >&2 " make docker up | make podman up"; \
|
echo >&2 " make docker up | make podman up"; \
|
||||||
echo >&2 " make docker down | make docker logs | make docker ps | make docker compose-build | make docker rebuild | make docker check-docker"; \
|
echo >&2 " make docker down | make docker logs | make docker ps | make docker compose-build | make docker rebuild | make docker check-docker | make docker kubectl CLUSTER=…"; \
|
||||||
echo >&2 " (или то же с префиксом podman)"; \
|
echo >&2 " (или то же с префиксом podman)"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@@ -77,6 +81,16 @@ check-docker: _require_runtime ## (с docker/podman) Проверить CLI и c
|
|||||||
@$(COMPOSE) version >/dev/null 2>&1 || { echo >&2 "Команда «$(COMPOSE) version» недоступна."; exit 1; }
|
@$(COMPOSE) version >/dev/null 2>&1 || { echo >&2 "Команда «$(COMPOSE) version» недоступна."; exit 1; }
|
||||||
@echo "$(COMPOSE): OK"
|
@echo "$(COMPOSE): OK"
|
||||||
|
|
||||||
|
# kubectl и kind в образе; kubeconfig в томе /work/clusters/<CLUSTER>/kubeconfig — kubectl на хосте не нужен.
|
||||||
|
kubectl: _require_runtime ## (с docker/podman) kubectl в контейнере: CLUSTER=имя [KUBECTL_ARGS="get pods -A"]
|
||||||
|
@if [ -z "$(CLUSTER)" ]; then \
|
||||||
|
echo >&2 "Задайте CLUSTER=<имя_кластера> (каталог в ./clusters/)."; \
|
||||||
|
echo >&2 "Пример: make docker kubectl CLUSTER=dev"; \
|
||||||
|
echo >&2 "Свои подкоманды: make docker kubectl CLUSTER=dev KUBECTL_ARGS=\"get pods -A\""; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
cd "$(KIND_K8S_DIR)" && $(COMPOSE) exec kind-k8s-web kubectl --kubeconfig=/work/clusters/$(CLUSTER)/kubeconfig $(KUBECTL_ARGS)
|
||||||
|
|
||||||
compose-build: _require_runtime clusters-dir ## (с docker/podman) Собрать образ kind-k8s-tools:local
|
compose-build: _require_runtime clusters-dir ## (с docker/podman) Собрать образ kind-k8s-tools:local
|
||||||
cd "$(KIND_K8S_DIR)" && $(COMPOSE) build $(COMPOSE_BUILD_FLAGS)
|
cd "$(KIND_K8S_DIR)" && $(COMPOSE) build $(COMPOSE_BUILD_FLAGS)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# kind-k8s-develop — локальные кластеры Kubernetes (kind)
|
# Kind Clusters Dashboard — локальные кластеры Kubernetes (kind)
|
||||||
|
|
||||||
Образ **kind-k8s-tools:local** и **Makefile** поднимают **веб-интерфейс** (FastAPI) на порту **8080** на хосте по умолчанию (или **`KIND_K8S_WEB_PORT`** в `.env`; внутри контейнера приложение слушает **6000**). Порт **6000 на хосте** не используем по умолчанию: Chrome и другие браузеры на Chromium отдают **ERR_UNSAFE_PORT**. Через браузер создаёте и удаляете кластеры, смотрите статистику и вывод `kubectl`. **kubeconfig** сохраняется в `clusters/<имя>/`. На хосте достаточно **Docker** (или Podman) и **make**; **kind** и **kubectl** — внутри контейнера.
|
Образ **kind-k8s-tools:local** и **Makefile** поднимают **веб-интерфейс** (FastAPI) на порту **8080** на хосте по умолчанию (или **`KIND_K8S_WEB_PORT`** в `.env`; внутри контейнера приложение слушает **6000**). Порт **6000 на хосте** не используем по умолчанию: Chrome и другие браузеры на Chromium отдают **ERR_UNSAFE_PORT**. Через браузер создаёте и удаляете кластеры, смотрите статистику и вывод `kubectl`. **kubeconfig** сохраняется в `clusters/<имя>/`. На хосте достаточно **Docker** (или Podman) и **make**; **kind** и **kubectl** — внутри контейнера.
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
- Быстро получить Kubernetes без облака (интеграционные тесты, манифесты, обучение).
|
- Быстро получить Kubernetes без облака (интеграционные тесты, манифесты, обучение).
|
||||||
- Версия кластера и число worker-нод задаются **в веб-UI** (или через REST API / скрипты в контейнере).
|
- Версия кластера и число worker-нод задаются **в веб-UI** (или через REST API / скрипты в контейнере).
|
||||||
- Количество кластеров **не ограничено** кодом (ограничения — ресурсы хоста и Docker).
|
- Количество кластеров **не ограничено** кодом (ограничения — ресурсы хоста и Docker).
|
||||||
- Артефакты на хосте: `clusters/<имя>/` — удобно указать путь к `kubeconfig` в приложении или в `kubectl`.
|
- Артефакты на хосте: `clusters/<имя>/` — том для `kubeconfig` (доступен в контейнере как `/work/clusters/<имя>/`).
|
||||||
|
|
||||||
## Веб-интерфейс
|
## Веб-интерфейс
|
||||||
|
|
||||||
@@ -43,9 +43,10 @@
|
|||||||
| **Docker** + **Compose v2** (или **Podman** + compose) | Сборка образа и запуск веб-сервиса |
|
| **Docker** + **Compose v2** (или **Podman** + compose) | Сборка образа и запуск веб-сервиса |
|
||||||
| **make** | `make docker up` / `make podman up` и вспомогательные цели |
|
| **make** | `make docker up` / `make podman up` и вспомогательные цели |
|
||||||
| **python3** | Только для **`make setup`** (создание `.env`) |
|
| **python3** | Только для **`make setup`** (создание `.env`) |
|
||||||
| **kubectl** (опционально) | Проверка API с хоста: `kubectl --kubeconfig=clusters/<имя>/kubeconfig get nodes` |
|
|
||||||
|
|
||||||
**В образ не обязательно ставить на хост:** kind, Python приложения — они внутри контейнера.
|
**На хост не нужны:** **kind**, **kubectl**, Python приложения — всё это в образе `kind-k8s-tools:local` и выполняется в контейнере **`kind-k8s-web`**. Проверка API и узлов: **веб-интерфейс** (кластер → узлы/поды) или **`make docker kubectl CLUSTER=<имя>`** / **`make podman kubectl …`** (см. ниже) — `kubectl` вызывается через **`docker compose exec`** / **`podman compose exec`** внутри уже запущенного сервиса.
|
||||||
|
|
||||||
|
Файл **`clusters/<имя>/kubeconfig`** на хосте можно использовать **опционально**, если у вас локально установлен kubectl (например IDE или отладка) — после патча apiserver обычно указывает на `127.0.0.1:<порт>`.
|
||||||
|
|
||||||
Смонтированы **сокет** Docker/Podman и каталог **`./clusters`** → в контейнере **`/work/clusters`**. Каталог **`./app`** монтируется в **`/opt/kind-k8s/app`** для разработки без пересборки образа. Файл **`./README.md`** монтируется в **`/opt/kind-k8s/README.md`** (страница **«Документация»** и **`GET /api/v1/docs/readme`** без пересборки образа).
|
Смонтированы **сокет** Docker/Podman и каталог **`./clusters`** → в контейнере **`/work/clusters`**. Каталог **`./app`** монтируется в **`/opt/kind-k8s/app`** для разработки без пересборки образа. Файл **`./README.md`** монтируется в **`/opt/kind-k8s/README.md`** (страница **«Документация»** и **`GET /api/v1/docs/readme`** без пересборки образа).
|
||||||
|
|
||||||
@@ -54,14 +55,14 @@
|
|||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd kind-k8s-develop
|
# Из корня клонированного репозитория Kind Clusters Dashboard (рядом с Makefile):
|
||||||
make setup # опционально: интерактивно создать .env (Enter — дефолты из скрипта)
|
make setup # опционально: интерактивно создать .env (Enter — дефолты из скрипта)
|
||||||
make docker check-docker # или: make podman check-docker
|
make docker check-docker # или: make podman check-docker
|
||||||
make docker up # или: make podman up
|
make docker up # или: make podman up
|
||||||
# Браузер: http://127.0.0.1:8080 (порт: KIND_K8S_WEB_PORT в .env; не 6000 на хосте — Chrome ERR_UNSAFE_PORT)
|
# Браузер: http://127.0.0.1:8080 (порт: KIND_K8S_WEB_PORT в .env; не 6000 на хосте — Chrome ERR_UNSAFE_PORT)
|
||||||
```
|
```
|
||||||
|
|
||||||
Из родительского каталога: `make -C kind-k8s-develop docker up`.
|
Из родительского каталога: `make -C <каталог-корня-репозитория> docker up` (подставьте путь к каталогу с `Makefile`).
|
||||||
|
|
||||||
**Логи, статус и остановка:** `make docker logs` / `make podman logs` (follow), `make docker ps` / `make podman ps`, `make docker down` / `make podman down`.
|
**Логи, статус и остановка:** `make docker logs` / `make podman logs` (follow), `make docker ps` / `make podman ps`, `make docker down` / `make podman down`.
|
||||||
|
|
||||||
@@ -87,7 +88,26 @@ docker compose run --rm --entrypoint python3 kind-k8s-web \
|
|||||||
|
|
||||||
Рабочий каталог сервиса в образе — `/opt/kind-k8s/app`; том `clusters/` и сокет те же, что у `docker compose up`.
|
Рабочий каталог сервиса в образе — `/opt/kind-k8s/app`; том `clusters/` и сокет те же, что у `docker compose up`.
|
||||||
|
|
||||||
После успешного `kind create` по умолчанию выполняется **`kubectl wait`** готовности нод (`KIND_K8S_WAIT_NODES`, `KIND_K8S_WAIT_NODES_TIMEOUT_SEC` в `.env`).
|
### kubectl без установки на хост
|
||||||
|
|
||||||
|
Пока запущен веб-сервис (`make docker up` или `make podman up`), **kubectl** из образа:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# По умолчанию: get nodes (kubeconfig: /work/clusters/<имя>/kubeconfig внутри контейнера)
|
||||||
|
make docker kubectl CLUSTER=<имя_кластера>
|
||||||
|
# или: make podman kubectl CLUSTER=<имя_кластера>
|
||||||
|
|
||||||
|
make docker kubectl CLUSTER=<имя> KUBECTL_ARGS="get pods -A"
|
||||||
|
make docker kubectl CLUSTER=<имя> KUBECTL_ARGS="config view --minify"
|
||||||
|
```
|
||||||
|
|
||||||
|
Эквивалент вручную (из корня репозитория, **Docker**):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec kind-k8s-web kubectl --kubeconfig=/work/clusters/<имя>/kubeconfig get nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
После успешного `kind create` по умолчанию выполняется **`kubectl wait`** готовности нод (`KIND_K8S_WAIT_NODES`, `KIND_K8S_WAIT_NODES_TIMEOUT_SEC` в `.env`) — тоже **внутри** контейнера приложения.
|
||||||
|
|
||||||
## Команды Makefile
|
## Команды Makefile
|
||||||
|
|
||||||
@@ -101,11 +121,12 @@ docker compose run --rm --entrypoint python3 kind-k8s-web \
|
|||||||
| `make docker compose-build` / `make podman compose-build` | Собрать образ `kind-k8s-tools:local` |
|
| `make docker compose-build` / `make podman compose-build` | Собрать образ `kind-k8s-tools:local` |
|
||||||
| `make docker rebuild` / `make podman rebuild` | Пересборка образа **без кэша** (`build --no-cache`) и пересоздание контейнера (`up -d --force-recreate`) |
|
| `make docker rebuild` / `make podman rebuild` | Пересборка образа **без кэша** (`build --no-cache`) и пересоздание контейнера (`up -d --force-recreate`) |
|
||||||
| `make docker check-docker` / `make podman check-docker` | Проверить выбранный CLI и `compose version` |
|
| `make docker check-docker` / `make podman check-docker` | Проверить выбранный CLI и `compose version` |
|
||||||
|
| `make docker kubectl CLUSTER=…` / `make podman kubectl CLUSTER=…` | **kubectl** в контейнере `kind-k8s-web` (опционально `KUBECTL_ARGS="…"`; по умолчанию `get nodes`). Сервис должен быть **up**. |
|
||||||
| `make setup` | Интерактивно создать `.env` (список переменных в `scripts/setup_env_interactive.py`) |
|
| `make setup` | Интерактивно создать `.env` (список переменных в `scripts/setup_env_interactive.py`) |
|
||||||
| `make clusters-dir` | Создать каталог `clusters/` |
|
| `make clusters-dir` | Создать каталог `clusters/` |
|
||||||
| `make docker …` / `make podman …` | Префикс **обязателен** для целей `up`, `down`, `logs`, `ps`, `compose-build`, `rebuild`, `check-docker` |
|
| `make docker …` / `make podman …` | Префикс **обязателен** для целей `up`, `down`, `logs`, `ps`, `compose-build`, `rebuild`, `check-docker`, `kubectl` |
|
||||||
|
|
||||||
Цели `up`, `down`, `logs`, `ps`, `compose-build`, `rebuild` и `check-docker` **без** `docker`/`podman` в той же команде завершатся с подсказкой.
|
Цели `up`, `down`, `logs`, `ps`, `compose-build`, `rebuild`, `check-docker` и `kubectl` **без** `docker`/`podman` в той же команде завершатся с подсказкой.
|
||||||
|
|
||||||
## Переменные окружения
|
## Переменные окружения
|
||||||
|
|
||||||
@@ -179,6 +200,6 @@ make podman up
|
|||||||
|
|
||||||
- Образ `kindest/node:v…` должен быть доступен для pull.
|
- Образ `kindest/node:v…` должен быть доступен для pull.
|
||||||
- На **Windows** без WSL удобнее WSL2 + Docker Desktop.
|
- На **Windows** без WSL удобнее WSL2 + Docker Desktop.
|
||||||
- Для проверки с хоста нужен отдельный **kubectl** (в образе kubectl только внутри контейнера).
|
- **kubectl** на хосте **не обязателен**: используйте веб-UI или **`make docker kubectl`** / **`make podman kubectl`** (см. выше).
|
||||||
- История заданий создания в UI/API хранится в памяти (до **200** записей); после перезапуска контейнера очищается.
|
- История заданий создания в UI/API хранится в памяти (до **200** записей); после перезапуска контейнера очищается.
|
||||||
- При **`exec format error`** у kind пересоберите образ: `make docker rebuild COMPOSE_BUILD_FLAGS=--platform linux/arm64` (или `make podman …`, или `compose-build` без `--no-cache`, или `linux/amd64`).
|
- При **`exec format error`** у kind пересоберите образ: `make docker rebuild COMPOSE_BUILD_FLAGS=--platform linux/arm64` (или `make podman …`, или `compose-build` без `--no-cache`, или `linux/amd64`).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Отдача сырого README.md для клиентского рендера Markdown (marked в static).
|
"""Отдача README.md и файлов ``app/docs/*.md`` для клиентского рендера Markdown (marked в static).
|
||||||
|
|
||||||
Автор: Сергей Антропов
|
Автор: Сергей Антропов
|
||||||
Сайт: https://devops.org.ru
|
Сайт: https://devops.org.ru
|
||||||
@@ -9,10 +9,10 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
|
|
||||||
from core.readme_doc import read_readme_text
|
from core.readme_doc import read_app_docs_file_text, read_readme_text
|
||||||
|
|
||||||
logger = logging.getLogger("kind_k8s.api.docs_readme")
|
logger = logging.getLogger("kind_k8s.api.docs_readme")
|
||||||
|
|
||||||
@@ -51,3 +51,40 @@ async def get_readme_markdown() -> PlainTextResponse:
|
|||||||
content=text,
|
content=text,
|
||||||
media_type="text/markdown; charset=utf-8",
|
media_type="text/markdown; charset=utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/docs/file",
|
||||||
|
response_class=PlainTextResponse,
|
||||||
|
summary="Файл Markdown под app/docs/",
|
||||||
|
responses={400: {"description": "Некорректный путь"}, 403: {"description": "Вне app/docs"}, 404: {"description": "Нет файла"}},
|
||||||
|
)
|
||||||
|
async def get_app_docs_file(
|
||||||
|
path: str = Query(
|
||||||
|
...,
|
||||||
|
description="Относительный путь, например app/docs/api_routes.md",
|
||||||
|
examples=["app/docs/api_routes.md"],
|
||||||
|
),
|
||||||
|
) -> PlainTextResponse:
|
||||||
|
"""
|
||||||
|
Только файлы ``*.md`` с префиксом ``app/docs/`` (без ``..``).
|
||||||
|
|
||||||
|
Ссылки из README ведут на ``/documentation?path=...``; клиент запрашивает этот эндпоинт.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
def _read() -> str:
|
||||||
|
return read_app_docs_file_text(path)
|
||||||
|
|
||||||
|
text = await asyncio.to_thread(_read)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.info("GET /docs/file: не найден или запрещён путь %s", path)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Файл не найден или путь не разрешён (ожидается app/docs/<имя>.md).",
|
||||||
|
) from None
|
||||||
|
logger.debug("GET /docs/file: отдан %s", path)
|
||||||
|
return PlainTextResponse(
|
||||||
|
content=text,
|
||||||
|
media_type="text/markdown; charset=utf-8",
|
||||||
|
)
|
||||||
|
|||||||
+11
-2
@@ -104,7 +104,7 @@ def _print_cluster(name: str, *, kube_path_saved: Path | None) -> None:
|
|||||||
use_path: str | None = None
|
use_path: str | None = None
|
||||||
if kube_path_saved and kube_path_saved.is_file():
|
if kube_path_saved and kube_path_saved.is_file():
|
||||||
use_path = str(kube_path_saved)
|
use_path = str(kube_path_saved)
|
||||||
print(" Проверка API: kubectl с сохранённым kubeconfig (как на хосте после создания кластера).")
|
print(" Проверка API: kubectl с сохранённым kubeconfig (тот же файл, что пишет create / UI).")
|
||||||
|
|
||||||
tmp_kc: str | None = None
|
tmp_kc: str | None = None
|
||||||
if not use_path:
|
if not use_path:
|
||||||
@@ -148,7 +148,16 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
sys.exit(127)
|
sys.exit(127)
|
||||||
if not shutil.which("kubectl"):
|
if not shutil.which("kubectl"):
|
||||||
print("Не найден kubectl.", file=sys.stderr)
|
print("Не найден kubectl в PATH.", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
" Запустите скрипт внутри контейнера приложения (там kubectl в образе), например:",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
" docker compose exec kind-k8s-web python3 /opt/kind-k8s/app/cluster_status.py <имя>",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(" Либо смотрите узлы в веб-интерфейсе; на хост kubectl не обязателен.", file=sys.stderr)
|
||||||
sys.exit(127)
|
sys.exit(127)
|
||||||
|
|
||||||
names = _kind_cluster_names()
|
names = _kind_cluster_names()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Вспомогательная логика для API и общих операций kind-k8s-develop.
|
"""Вспомогательная логика для API и общих операций Kind Clusters Dashboard.
|
||||||
|
|
||||||
Автор: Сергей Антропов
|
Автор: Сергей Антропов
|
||||||
Сайт: https://devops.org.ru
|
Сайт: https://devops.org.ru
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
_DEFAULT_TITLE = "kind-k8s-develop"
|
_DEFAULT_TITLE = "Kind Clusters Dashboard"
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|||||||
+44
-3
@@ -1,10 +1,11 @@
|
|||||||
"""Чтение README.md для API ``GET /api/v1/docs/readme`` и страницы «Документация».
|
"""Чтение README.md и безопасное чтение ``app/docs/*.md`` для API документации.
|
||||||
|
|
||||||
Разметка Markdown преобразуется в браузере: ``/static/js/vendor/marked.min.js`` и
|
Разметка Markdown преобразуется в браузере: ``/static/js/vendor/marked.min.js`` и
|
||||||
``purify.min.js`` (файлы входят в репозиторий, без CDN).
|
``purify.min.js`` (файлы входят в репозиторий, без CDN).
|
||||||
|
|
||||||
Путь к файлу: ``KIND_K8S_README_PATH`` или ``README.md`` в корне рядом с ``app/``;
|
README: ``KIND_K8S_README_PATH`` или ``README.md`` в корне рядом с ``app/``;
|
||||||
в Docker-образе — ``/opt/kind-k8s/README.md``.
|
в 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
|
Сайт: https://devops.org.ru
|
||||||
@@ -86,3 +87,43 @@ def read_readme_text() -> str:
|
|||||||
[str(x) for x in _candidates_without_env()],
|
[str(x) for x in _candidates_without_env()],
|
||||||
)
|
)
|
||||||
raise FileNotFoundError("README.md")
|
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
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ def _run_interactive() -> None:
|
|||||||
if not _which("kind"):
|
if not _which("kind"):
|
||||||
print("Не найден бинарник kind.", file=sys.stderr)
|
print("Не найден бинарник kind.", file=sys.stderr)
|
||||||
print(" Установка kind на хост: https://kind.sigs.k8s.io/docs/user/quick-start/#installation", file=sys.stderr)
|
print(" Установка kind на хост: https://kind.sigs.k8s.io/docs/user/quick-start/#installation", file=sys.stderr)
|
||||||
print(" Через Docker: make -C kind-k8s-develop docker up и веб-интерфейс.", file=sys.stderr)
|
print(" Через Docker: из корня репозитория выполните «make docker up» и откройте веб-интерфейс.", file=sys.stderr)
|
||||||
sys.exit(127)
|
sys.exit(127)
|
||||||
cli = _container_cli_bin()
|
cli = _container_cli_bin()
|
||||||
if not _which(cli):
|
if not _which(cli):
|
||||||
@@ -194,13 +194,15 @@ def _run_interactive() -> None:
|
|||||||
print("\nГотово.")
|
print("\nГотово.")
|
||||||
print(f" kubeconfig (в среде запуска): {result.kubeconfig_path}")
|
print(f" kubeconfig (в среде запуска): {result.kubeconfig_path}")
|
||||||
if _in_container():
|
if _in_container():
|
||||||
print(f" Том на хосте: kind-k8s-develop/clusters/{result.cluster_name}/ (рядом с Makefile)")
|
print(f" Том на хосте: clusters/{result.cluster_name}/ в корне репозитория (рядом с Makefile)")
|
||||||
print(
|
print(
|
||||||
f' Проверка с хоста (из каталога репозитория): kubectl --kubeconfig="$(pwd)/clusters/{result.cluster_name}/kubeconfig" get nodes',
|
f" Проверка в этом контейнере: kubectl --kubeconfig=/work/clusters/{result.cluster_name}/kubeconfig get nodes",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f" Проверка: KUBECONFIG={result.kubeconfig_path} kubectl get nodes")
|
print(" Проверка без kubectl на хосте: веб-интерфейс (кластер → узлы/поды) или из корня репозитория:")
|
||||||
print(f" Или: kubectl --kubeconfig={result.kubeconfig_path} get nodes")
|
print(f" make docker kubectl CLUSTER={result.cluster_name} # или: make podman kubectl …")
|
||||||
|
print(" (сервис kind-k8s-web должен быть запущен: make docker up).")
|
||||||
|
print(f" Локально при установленном kubectl: kubectl --kubeconfig={result.kubeconfig_path} get nodes")
|
||||||
if result.kubeconfig_patched_for_host:
|
if result.kubeconfig_patched_for_host:
|
||||||
print(" apiserver настроен на 127.0.0.1:<порт> для доступа с хоста.")
|
print(" apiserver настроен на 127.0.0.1:<порт> для доступа с хоста.")
|
||||||
if result.nodes_ready is False and result.nodes_ready_message:
|
if result.nodes_ready is False and result.nodes_ready_message:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def _interactive() -> None:
|
|||||||
CLUSTERS_DIR = clusters_dir()
|
CLUSTERS_DIR = clusters_dir()
|
||||||
if not shutil.which("kind"):
|
if not shutil.which("kind"):
|
||||||
print("Не найден kind.", file=sys.stderr)
|
print("Не найден kind.", file=sys.stderr)
|
||||||
print(" Через Docker: make -C kind-k8s-develop docker up и удаление в веб-интерфейсе.", file=sys.stderr)
|
print(" Через Docker: из корня репозитория «make docker up» и удаление в веб-интерфейсе.", file=sys.stderr)
|
||||||
sys.exit(127)
|
sys.exit(127)
|
||||||
|
|
||||||
clusters = _list_kind_clusters()
|
clusters = _list_kind_clusters()
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
|------|------------|
|
|------|------------|
|
||||||
| [api_routes.md](api_routes.md) | Полное описание REST API `/api/v1/*` с примерами JSON (ориентир для фронтенда и клиентов). |
|
| [api_routes.md](api_routes.md) | Полное описание REST API `/api/v1/*` с примерами JSON (ориентир для фронтенда и клиентов). |
|
||||||
|
|
||||||
После запуска: **Swagger** — `/docs`, **ReDoc** — `/redoc`, **Health** — `/api/v1/health` (тот же порт, что и UI). С дашборда эти ссылки открываются в **отдельном окне** браузера.
|
После запуска: **Swagger** — `/docs`, **ReDoc** — `/redoc`, **Health** — `/api/v1/health` (тот же порт, что и UI). С дашборда эти ссылки открываются в **отдельном окне** браузера. **kubectl** на машине разработчика не нужен: он в образе; см. **README.md** — цель **`make docker kubectl`** / **`make podman kubectl`** и API **`/api/v1/clusters/{name}/workloads`**.
|
||||||
|
|
||||||
**Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)
|
**Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)
|
||||||
|
|||||||
+34
-5
@@ -1,4 +1,4 @@
|
|||||||
# Описание REST API веб-интерфейса kind-k8s-develop
|
# Описание REST API веб-интерфейса Kind Clusters Dashboard
|
||||||
|
|
||||||
**Базовый префикс:** `/api/v1`
|
**Базовый префикс:** `/api/v1`
|
||||||
**Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)
|
**Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
| Swagger UI (OpenAPI) | `http://127.0.0.1:<порт>/docs` (порт на хосте по умолчанию **8080**, см. `KIND_K8S_WEB_PORT`; 6000 на хосте блокируется Chrome) |
|
| Swagger UI (OpenAPI) | `http://127.0.0.1:<порт>/docs` (порт на хосте по умолчанию **8080**, см. `KIND_K8S_WEB_PORT`; 6000 на хосте блокируется Chrome) |
|
||||||
| ReDoc | `http://127.0.0.1:<порт>/redoc` |
|
| ReDoc | `http://127.0.0.1:<порт>/redoc` |
|
||||||
| Health (JSON) | `http://127.0.0.1:<порт>/api/v1/health` |
|
| Health (JSON) | `http://127.0.0.1:<порт>/api/v1/health` |
|
||||||
| Документация проекта | `http://127.0.0.1:<порт>/documentation` — **README.md**: текст с `GET /api/v1/docs/readme`, рендер **Markdown** в браузере (**marked** + **DOMPurify** из `app/static/js/vendor/`, без CDN) |
|
| Документация проекта | `http://127.0.0.1:<порт>/documentation` — по умолчанию **README** (`GET /api/v1/docs/readme`); ссылки на `app/docs/*.md` открываются в той же странице (`?path=...` + `GET /api/v1/docs/file`); рендер **Markdown** в браузере (**marked** + **DOMPurify** из `app/static/js/vendor/`, без CDN) |
|
||||||
| Этот файл | `app/docs/api_routes.md` в репозитории |
|
| Этот файл | `app/docs/api_routes.md` в репозитории |
|
||||||
|
|
||||||
С **веб-панели** (`GET /`) пункты меню **Swagger**, **ReDoc** и **Health** вызывают `window.open` с именами окон `kind_swagger`, `kind_redoc`, `kind_health` (отдельное окно, повторный клик переиспользует то же окно). Пункт **Документация** открывает `GET /documentation` в той же вкладке.
|
С **веб-панели** (`GET /`) пункты меню **Swagger**, **ReDoc** и **Health** вызывают `window.open` с именами окон `kind_swagger`, `kind_redoc`, `kind_health` (отдельное окно, повторный клик переиспользует то же окно). Пункт **Документация** открывает `GET /documentation` в той же вкладке.
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
| Маршрут | Описание |
|
| Маршрут | Описание |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| `GET /` | HTML-панель: единая карточка «панель + среда», статистика, создание кластера (прогресс, **журнал** `kind create`, отмена), таблица кластеров с **иконками** действий и **всплывающими подсказками**, модалка узлов/подов; шапка — пилюли, Swagger / ReDoc / Health в отдельных окнах. |
|
| `GET /` | HTML-панель: единая карточка «панель + среда», статистика, создание кластера (прогресс, **журнал** `kind create`, отмена), таблица кластеров с **иконками** действий и **всплывающими подсказками**, модалка узлов/подов; шапка — пилюли, Swagger / ReDoc / Health в отдельных окнах. |
|
||||||
| `GET /documentation` | HTML-оболочка; контент — запрос к **`GET /api/v1/docs/readme`** и разбор Markdown скриптами из **`/static/js/vendor/`** (marked, DOMPurify). Путь к README: `KIND_K8S_README_PATH` или `README.md` рядом с `app/`; в образе — `/opt/kind-k8s/README.md`. |
|
| `GET /documentation` | HTML-оболочка; **`documentation.js`**: без `path` — **`GET /api/v1/docs/readme`**, с `?path=app/docs/…` — **`GET /api/v1/docs/file`**; разбор Markdown из **`/static/js/vendor/`** (marked, DOMPurify). Секции **H2** показываются отдельными карточками (заголовок и тело). В шапке активна пилюля **Документация**. Путь к README: `KIND_K8S_README_PATH` или `README.md` рядом с `app/`; в образе — `/opt/kind-k8s/README.md`. |
|
||||||
| `GET /ui` | Редирект **307** на `/` (удобный ярлык). |
|
| `GET /ui` | Редирект **307** на `/` (удобный ярлык). |
|
||||||
| `GET /static/…` | CSS (`style.css`), скрипт панели (`js/dashboard.js`); базовый URL API задаётся атрибутом `data-api-base` на `<body>` (по умолчанию `/api/v1`). |
|
| `GET /static/…` | CSS (`style.css`), скрипты панели (`js/dashboard.js`) и документации (`js/documentation.js`); базовый URL API задаётся атрибутом `data-api-base` на `<body>` (по умолчанию `/api/v1`). |
|
||||||
|
|
||||||
Шаблоны: `app/templates/base.html` (шапка, навигация), `app/templates/dashboard.html` (контент панели), `app/templates/documentation.html` (README).
|
Шаблоны: `app/templates/base.html` (шапка, навигация), `app/templates/dashboard.html` (контент панели), `app/templates/documentation.html` (README).
|
||||||
|
|
||||||
|
**kubectl на хосте не обязателен:** бинарник есть в образе; узлы и поды доступны через API (**`GET /api/v1/clusters/{name}/workloads`**) и веб-UI. Для интерактивной консоли из корня репозитория при запущенном compose: **`make docker kubectl CLUSTER=<имя>`** (или **`make podman kubectl …`**), внутри контейнера kubeconfig — **`/work/clusters/<имя>/kubeconfig`**. Подробности — **README.md** (раздел «kubectl без установки на хост»).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Сводка маршрутов API
|
## Сводка маршрутов API
|
||||||
@@ -33,7 +35,8 @@
|
|||||||
| Метод | Путь | Кратко |
|
| Метод | Путь | Кратко |
|
||||||
|-------|------|--------|
|
|-------|------|--------|
|
||||||
| GET | `/api/v1/health` | Среда: kind, kubectl, движок контейнеров |
|
| GET | `/api/v1/health` | Среда: kind, kubectl, движок контейнеров |
|
||||||
| GET | `/api/v1/docs/readme` | Текст **README.md** (`text/markdown`; для страницы `/documentation`) |
|
| GET | `/api/v1/docs/readme` | Текст **README.md** (`text/markdown`; страница `/documentation` без `path`) |
|
||||||
|
| GET | `/api/v1/docs/file` | Текст одного **`.md`** под `app/docs/` (query `path=app/docs/…`; для `/documentation?path=…`) |
|
||||||
| GET | `/api/v1/versions` | Теги `kindest/node` (Docker Hub) или пусто при `KIND_K8S_SKIP_VERSION_LIST` |
|
| GET | `/api/v1/versions` | Теги `kindest/node` (Docker Hub) или пусто при `KIND_K8S_SKIP_VERSION_LIST` |
|
||||||
| GET | `/api/v1/stats` | Сводка для дашборда |
|
| GET | `/api/v1/stats` | Сводка для дашборда |
|
||||||
| GET | `/api/v1/clusters` | Список кластеров |
|
| GET | `/api/v1/clusters` | Список кластеров |
|
||||||
@@ -103,6 +106,32 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## GET /api/v1/docs/file
|
||||||
|
|
||||||
|
Параметр запроса **`path`** — относительный путь вида **`app/docs/<имя>.md`**. Допускаются только такие пути (префикс `app/docs/`, расширение `.md`, без `..`); иначе **404**.
|
||||||
|
|
||||||
|
Тело ответа — UTF-8 Markdown, **`Content-Type: text/markdown; charset=utf-8`**.
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/docs/file?path=app%2Fdocs%2Fapi_routes.md HTTP/1.1
|
||||||
|
Host: 127.0.0.1:8080
|
||||||
|
Accept: text/markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример начала тела ответа 200** (не JSON, текст Markdown):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Описание REST API веб-интерфейса Kind Clusters Dashboard
|
||||||
|
|
||||||
|
**Базовый префикс:** `/api/v1`
|
||||||
|
```
|
||||||
|
|
||||||
|
Логика проверки пути: `app/core/readme_doc.py` (`resolve_app_docs_markdown`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## GET /api/v1/versions
|
## GET /api/v1/versions
|
||||||
|
|
||||||
Список стабильных тегов `kindest/node` с Docker Hub (для выпадающего списка в UI).
|
Список стабильных тегов `kindest/node` с Docker Hub (для выпадающего списка в UI).
|
||||||
|
|||||||
+2
-2
@@ -71,7 +71,7 @@ async def dashboard(request: Request) -> HTMLResponse:
|
|||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"dashboard.html",
|
"dashboard.html",
|
||||||
{"app_title": settings.app_title},
|
{"app_title": settings.app_title, "nav_active": "panel"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,5 +92,5 @@ async def documentation_page(request: Request) -> HTMLResponse:
|
|||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"documentation.html",
|
"documentation.html",
|
||||||
{"app_title": settings.app_title},
|
{"app_title": settings.app_title, "nav_active": "documentation"},
|
||||||
)
|
)
|
||||||
|
|||||||
+212
-31
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Страница /documentation: загрузка README через API и рендер Markdown.
|
* Страница /documentation: README и файлы app/docs/*.md через API, рендер Markdown.
|
||||||
* Зависимости из репозитория: /static/js/vendor/marked.min.js, purify.min.js
|
* Зависимости из репозитория: /static/js/vendor/marked.min.js, purify.min.js
|
||||||
*
|
*
|
||||||
* Автор: Сергей Антропов
|
* Автор: Сергей Антропов
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const body = document.body;
|
var body = document.body;
|
||||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
var API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||||
|
var DOC_BASE = "/documentation";
|
||||||
|
|
||||||
function showErr(el, msg) {
|
function showErr(el, msg) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -17,56 +18,236 @@
|
|||||||
el.classList.remove("hidden");
|
el.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
function hideErr(el) {
|
||||||
const article = document.getElementById("readme-doc-article");
|
if (!el) return;
|
||||||
const errEl = document.getElementById("readme-error");
|
el.textContent = "";
|
||||||
const loadEl = document.getElementById("readme-loading");
|
el.classList.add("hidden");
|
||||||
if (!article || !loadEl) return;
|
}
|
||||||
|
|
||||||
|
function getPathFromLocation() {
|
||||||
|
var q = new URLSearchParams(window.location.search || "");
|
||||||
|
var p = (q.get("path") || "").trim();
|
||||||
|
return p || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function docUrlForPath(relPath) {
|
||||||
|
if (!relPath) return DOC_BASE;
|
||||||
|
return DOC_BASE + "?path=" + encodeURIComponent(relPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Внутренняя ссылка на .md под app/docs/: полный путь или относительно текущего файла.
|
||||||
|
*/
|
||||||
|
function resolveInternalMdHref(href, currentDocPath) {
|
||||||
|
if (!href) return null;
|
||||||
|
var h = href.trim();
|
||||||
|
if (h.startsWith("#")) return null;
|
||||||
|
if (/^https?:\/\//i.test(h)) return null;
|
||||||
|
if (/^mailto:/i.test(h)) return null;
|
||||||
|
|
||||||
|
var abs = h.match(/^(\.\/)?(app\/docs\/[^?#]+\.md)([\?#].*)?$/i);
|
||||||
|
if (abs) {
|
||||||
|
return { path: abs[2].replace(/\\/g, "/"), tail: abs[3] || "" };
|
||||||
|
}
|
||||||
|
if (!currentDocPath || currentDocPath.indexOf("app/docs/") !== 0) return null;
|
||||||
|
if (h.indexOf("..") >= 0) return null;
|
||||||
|
|
||||||
|
var baseDir = currentDocPath.replace(/[^/]+\.md$/i, "");
|
||||||
|
if (!baseDir) baseDir = "app/docs/";
|
||||||
|
try {
|
||||||
|
var u = new URL(h, "http://doc.local/" + baseDir);
|
||||||
|
var pathname = decodeURIComponent(u.pathname.replace(/^\/+/, ""));
|
||||||
|
if (pathname.indexOf("..") >= 0) return null;
|
||||||
|
if (!pathname.startsWith("app/docs/") || !pathname.endsWith(".md")) return null;
|
||||||
|
var tail = (u.hash || "") + (u.search || "");
|
||||||
|
return { path: pathname, tail: tail };
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteMdLinks(container, currentDocPath) {
|
||||||
|
container.querySelectorAll("a[href]").forEach(function (a) {
|
||||||
|
var href = a.getAttribute("href");
|
||||||
|
var resolved = resolveInternalMdHref(href, currentDocPath);
|
||||||
|
if (!resolved) return;
|
||||||
|
a.setAttribute("href", docUrlForPath(resolved.path) + resolved.tail);
|
||||||
|
a.classList.add("doc-md-link");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* До первого H2 — одна карточка; для каждого H2 — карточка заголовка и карточка тела до следующего H2.
|
||||||
|
*/
|
||||||
|
function sectionizeFromNodes(sourceRoot) {
|
||||||
|
var page = document.createElement("div");
|
||||||
|
page.className = "readme-doc-page-inner";
|
||||||
|
var root = document.createElement("div");
|
||||||
|
root.className = "readme-doc-root";
|
||||||
|
page.appendChild(root);
|
||||||
|
|
||||||
|
var nodes = Array.prototype.slice.call(sourceRoot.childNodes);
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
function appendCard(classExtra, chunk) {
|
||||||
|
if (!chunk.length) return;
|
||||||
|
var card = document.createElement("section");
|
||||||
|
card.className = "card readme-section-card markdown-body " + classExtra;
|
||||||
|
chunk.forEach(function (n) {
|
||||||
|
card.appendChild(n);
|
||||||
|
});
|
||||||
|
root.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushIntro() {
|
||||||
|
var chunk = [];
|
||||||
|
while (i < nodes.length) {
|
||||||
|
var n = nodes[i];
|
||||||
|
if (n.nodeType === 1 && n.tagName === "H2") break;
|
||||||
|
chunk.push(n);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
appendCard("readme-section-card--intro", chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushH2Pair() {
|
||||||
|
if (i >= nodes.length) return;
|
||||||
|
var h2 = nodes[i];
|
||||||
|
if (h2.nodeType !== 1 || h2.tagName !== "H2") return;
|
||||||
|
i++;
|
||||||
|
appendCard("readme-section-card--head", [h2]);
|
||||||
|
|
||||||
|
var bodyChunk = [];
|
||||||
|
while (i < nodes.length) {
|
||||||
|
var n = nodes[i];
|
||||||
|
if (n.nodeType === 1 && n.tagName === "H2") break;
|
||||||
|
bodyChunk.push(n);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
appendCard("readme-section-card--body", bodyChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
flushIntro();
|
||||||
|
while (i < nodes.length) flushH2Pair();
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl) {
|
||||||
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
|
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
|
||||||
showErr(errEl, "Не загружены скрипты marked или DOMPurify из /static/js/vendor/.");
|
showErr(errEl, "Не загружены скрипты marked или DOMPurify из /static/js/vendor/.");
|
||||||
loadEl.classList.add("hidden");
|
if (loadEl) loadEl.classList.add("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(API + "/docs/readme", {
|
|
||||||
headers: { Accept: "text/markdown, text/plain, */*" },
|
|
||||||
});
|
|
||||||
const text = await r.text();
|
|
||||||
if (!r.ok) {
|
|
||||||
var detail = text;
|
|
||||||
try {
|
|
||||||
var j = JSON.parse(text);
|
|
||||||
if (j.detail) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
|
|
||||||
} catch (e) {
|
|
||||||
/* сырой текст */
|
|
||||||
}
|
|
||||||
showErr(errEl, "Не удалось загрузить README: " + (detail || r.statusText));
|
|
||||||
loadEl.classList.add("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* marked v15: переносы строк в параграфах (breaks). */
|
|
||||||
try {
|
try {
|
||||||
if (marked.defaults && typeof marked.defaults === "object") {
|
if (marked.defaults && typeof marked.defaults === "object") {
|
||||||
marked.defaults.breaks = true;
|
marked.defaults.breaks = true;
|
||||||
marked.defaults.gfm = true;
|
marked.defaults.gfm = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e1) {
|
||||||
/* игнорируем, если defaults защищены от записи */
|
/* ignore */
|
||||||
}
|
}
|
||||||
var rawHtml =
|
var rawHtml =
|
||||||
typeof marked.parse === "function" ? marked.parse(text, { async: false }) : marked(text);
|
typeof marked.parse === "function" ? marked.parse(text, { async: false }) : marked(text);
|
||||||
article.innerHTML = DOMPurify.sanitize(rawHtml);
|
|
||||||
article.removeAttribute("hidden");
|
var temp = document.createElement("div");
|
||||||
|
temp.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||||
|
rewriteMdLinks(temp, docPath);
|
||||||
|
|
||||||
|
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
|
||||||
|
var pageInner = sectionizeFromNodes(temp);
|
||||||
|
rootEl.appendChild(pageInner);
|
||||||
|
rootEl.removeAttribute("hidden");
|
||||||
|
} catch (e2) {
|
||||||
|
showErr(errEl, "Ошибка разбора: " + (e2.message || String(e2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchUrlForPath(docPath) {
|
||||||
|
if (!docPath) return API + "/docs/readme";
|
||||||
|
return API + "/docs/file?path=" + encodeURIComponent(docPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDocumentation(docPath, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var push = opts.pushState === true;
|
||||||
|
var replace = opts.replaceState === true;
|
||||||
|
|
||||||
|
var rootEl = document.getElementById("readme-doc-root");
|
||||||
|
var errEl = document.getElementById("readme-error");
|
||||||
|
var loadEl = document.getElementById("readme-loading");
|
||||||
|
if (!rootEl || !loadEl) return;
|
||||||
|
|
||||||
|
hideErr(errEl);
|
||||||
|
rootEl.setAttribute("hidden", "hidden");
|
||||||
|
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
|
||||||
|
loadEl.classList.remove("hidden");
|
||||||
|
|
||||||
|
var url = fetchUrlForPath(docPath);
|
||||||
|
try {
|
||||||
|
var r = await fetch(url, {
|
||||||
|
headers: { Accept: "text/markdown, text/plain, */*" },
|
||||||
|
});
|
||||||
|
var text = await r.text();
|
||||||
|
if (!r.ok) {
|
||||||
|
var detail = text;
|
||||||
|
try {
|
||||||
|
var j = JSON.parse(text);
|
||||||
|
if (j.detail) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
|
||||||
|
} catch (e0) {
|
||||||
|
/* raw */
|
||||||
|
}
|
||||||
|
showErr(errEl, "Не удалось загрузить документ: " + (detail || r.statusText));
|
||||||
loadEl.classList.add("hidden");
|
loadEl.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl);
|
||||||
|
loadEl.classList.add("hidden");
|
||||||
|
|
||||||
|
var newUrl = docUrlForPath(docPath);
|
||||||
|
if (push) {
|
||||||
|
window.history.pushState({ docPath: docPath }, "", newUrl);
|
||||||
|
} else if (replace) {
|
||||||
|
window.history.replaceState({ docPath: docPath }, "", newUrl);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErr(errEl, "Ошибка: " + (e.message || String(e)));
|
showErr(errEl, "Ошибка: " + (e.message || String(e)));
|
||||||
loadEl.classList.add("hidden");
|
loadEl.classList.add("hidden");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPopState() {
|
||||||
|
loadDocumentation(getPathFromLocation(), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRootClick(e) {
|
||||||
|
var a = e.target && e.target.closest ? e.target.closest("a.doc-md-link") : null;
|
||||||
|
if (!a || !a.getAttribute("href")) return;
|
||||||
|
var href = a.getAttribute("href");
|
||||||
|
if (href.indexOf(DOC_BASE) !== 0) return;
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
var u = new URL(href, window.location.origin);
|
||||||
|
var p = (u.searchParams.get("path") || "").trim() || null;
|
||||||
|
loadDocumentation(p, { pushState: true });
|
||||||
|
} catch (e1) {
|
||||||
|
loadDocumentation(getPathFromLocation(), { pushState: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
var rootEl = document.getElementById("readme-doc-root");
|
||||||
|
if (!rootEl) return;
|
||||||
|
|
||||||
|
window.addEventListener("popstate", onPopState);
|
||||||
|
rootEl.addEventListener("click", onRootClick);
|
||||||
|
|
||||||
|
var initial = getPathFromLocation();
|
||||||
|
loadDocumentation(initial, { replaceState: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", run);
|
document.addEventListener("DOMContentLoaded", run);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+66
-40
@@ -1,4 +1,4 @@
|
|||||||
/* Минимальные стили веб-интерфейса kind-k8s-develop.
|
/* Минимальные стили веб-интерфейса Kind Clusters Dashboard.
|
||||||
Автор: Сергей Антропов — https://devops.org.ru */
|
Автор: Сергей Антропов — https://devops.org.ru */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -146,6 +146,21 @@ body.modal-open {
|
|||||||
background: linear-gradient(145deg, rgba(59, 130, 246, 0.35), rgba(59, 130, 246, 0.15));
|
background: linear-gradient(145deg, rgba(59, 130, 246, 0.35), rgba(59, 130, 246, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Активный раздел (панель или документация) */
|
||||||
|
.nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext) {
|
||||||
|
border-color: rgba(59, 130, 246, 0.85);
|
||||||
|
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35), 0 2px 14px rgba(59, 130, 246, 0.18);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
.nav-link.nav-pill.nav-pill--home.nav-pill--active {
|
||||||
|
background: linear-gradient(145deg, rgba(59, 130, 246, 0.42), rgba(59, 130, 246, 0.18));
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext) {
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Внешние окна: иконка «новое окно» */
|
/* Внешние окна: иконка «новое окно» */
|
||||||
.nav-link.nav-pill--ext::after {
|
.nav-link.nav-pill--ext::after {
|
||||||
content: "↗";
|
content: "↗";
|
||||||
@@ -887,78 +902,89 @@ button.btn-danger:hover {
|
|||||||
filter: brightness(1.12);
|
filter: brightness(1.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Страница «Документация»: README.md → HTML (Markdown) */
|
/* Страница «Документация»: Markdown → карточки секций */
|
||||||
.readme-doc-card {
|
.readme-doc-shell {
|
||||||
max-width: 52rem;
|
max-width: 52rem;
|
||||||
margin: 0 auto 2rem;
|
margin: 0 auto;
|
||||||
}
|
|
||||||
.readme-doc-toolbar {
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
}
|
|
||||||
.readme-doc-toolbar .page-title {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.readme-doc-lead {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
}
|
||||||
.readme-doc-loading {
|
.readme-doc-loading {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 0.75rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body {
|
.readme-doc-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.readme-doc-page-inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.readme-doc-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.readme-section-card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.readme-section-card--head h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.readme-section-card.markdown-body {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body h1,
|
.readme-section-card.markdown-body h1,
|
||||||
.readme-doc.markdown-body h2,
|
.readme-section-card.markdown-body h2,
|
||||||
.readme-doc.markdown-body h3,
|
.readme-section-card.markdown-body h3,
|
||||||
.readme-doc.markdown-body h4 {
|
.readme-section-card.markdown-body h4 {
|
||||||
margin: 1.25rem 0 0.5rem;
|
margin: 1.25rem 0 0.5rem;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body h1 {
|
.readme-section-card.markdown-body h1 {
|
||||||
font-size: 1.45rem;
|
font-size: 1.45rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
padding-bottom: 0.35rem;
|
padding-bottom: 0.35rem;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body h2 {
|
.readme-section-card.markdown-body h2 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body h3 {
|
.readme-section-card--head.markdown-body h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.readme-section-card.markdown-body h3 {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body p {
|
.readme-section-card.markdown-body p {
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body ul,
|
.readme-section-card.markdown-body ul,
|
||||||
.readme-doc.markdown-body ol {
|
.readme-section-card.markdown-body ol {
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
padding-left: 1.35rem;
|
padding-left: 1.35rem;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body li {
|
.readme-section-card.markdown-body li {
|
||||||
margin: 0.2rem 0;
|
margin: 0.2rem 0;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body a {
|
.readme-section-card.markdown-body a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 2px;
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body a:hover {
|
.readme-section-card.markdown-body a:hover {
|
||||||
filter: brightness(1.15);
|
filter: brightness(1.15);
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body code {
|
.readme-section-card.markdown-body code {
|
||||||
font-size: 0.88em;
|
font-size: 0.88em;
|
||||||
padding: 0.12em 0.35em;
|
padding: 0.12em 0.35em;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: rgba(0, 0, 0, 0.22);
|
background: rgba(0, 0, 0, 0.22);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body pre {
|
.readme-section-card.markdown-body pre {
|
||||||
margin: 0.65rem 0;
|
margin: 0.65rem 0;
|
||||||
padding: 0.65rem 0.85rem;
|
padding: 0.65rem 0.85rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -968,45 +994,45 @@ button.btn-danger:hover {
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body pre code {
|
.readme-section-card.markdown-body pre code {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body blockquote {
|
.readme-section-card.markdown-body blockquote {
|
||||||
margin: 0.65rem 0;
|
margin: 0.65rem 0;
|
||||||
padding: 0.35rem 0.75rem;
|
padding: 0.35rem 0.75rem;
|
||||||
border-left: 4px solid var(--accent);
|
border-left: 4px solid var(--accent);
|
||||||
background: rgba(59, 130, 246, 0.08);
|
background: rgba(59, 130, 246, 0.08);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body table {
|
.readme-section-card.markdown-body table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin: 0.75rem 0;
|
margin: 0.75rem 0;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body th,
|
.readme-section-card.markdown-body th,
|
||||||
.readme-doc.markdown-body td {
|
.readme-section-card.markdown-body td {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 0.4rem 0.55rem;
|
padding: 0.4rem 0.55rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body th {
|
.readme-section-card.markdown-body th {
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, 0.15);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body hr {
|
.readme-section-card.markdown-body hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
margin: 1.25rem 0;
|
margin: 1.25rem 0;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.readme-doc.markdown-body code {
|
.readme-section-card.markdown-body code {
|
||||||
background: rgba(0, 0, 0, 0.06);
|
background: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
.readme-doc.markdown-body pre {
|
.readme-section-card.markdown-body pre {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{# Общий каркас страниц веб-интерфейса kind-k8s-develop.
|
{# Общий каркас страниц веб-интерфейса Kind Clusters Dashboard.
|
||||||
Автор: Сергей Антропов — https://devops.org.ru #}
|
Автор: Сергей Антропов — https://devops.org.ru #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
<span class="nav-title">{{ app_title }}</span>
|
<span class="nav-title">{{ app_title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav-links" aria-label="Разделы">
|
<nav class="nav-links" aria-label="Разделы">
|
||||||
<a href="/" class="nav-link nav-pill nav-pill--home">Панель</a>
|
<a href="/" class="nav-link nav-pill nav-pill--home{% if nav_active|default('') == 'panel' %} nav-pill--active{% endif %}">Панель</a>
|
||||||
<a href="/documentation" class="nav-link nav-pill">Документация</a>
|
<a href="/documentation" class="nav-link nav-pill{% if nav_active|default('') == 'documentation' %} nav-pill--active{% endif %}">Документация</a>
|
||||||
<a href="/docs" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_swagger">Swagger</a>
|
<a href="/docs" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_swagger">Swagger</a>
|
||||||
<a href="/redoc" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_redoc">ReDoc</a>
|
<a href="/redoc" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_redoc">ReDoc</a>
|
||||||
<a href="/api/v1/health" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_health">Health</a>
|
<a href="/api/v1/health" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_health">Health</a>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<p class="muted footer-line">Данные: том <code>clusters/</code> на хосте</p>
|
<p class="muted footer-line">Данные: том <code>clusters/</code> на хосте</p>
|
||||||
<p class="footer-copyright">
|
<p class="footer-copyright">
|
||||||
© kind-k8s-develop ·
|
© {{ app_title }} ·
|
||||||
<a href="https://devops.org.ru" target="_blank" rel="noopener">devops.org.ru</a>
|
<a href="https://devops.org.ru" target="_blank" rel="noopener">devops.org.ru</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
{# Документация: README.md через GET /api/v1/docs/readme + marked + DOMPurify из static/vendor.
|
{# Документация: README и app/docs/*.md через API + marked + DOMPurify (vendor, без CDN).
|
||||||
Автор: Сергей Антропов — https://devops.org.ru #}
|
Автор: Сергей Антропов — https://devops.org.ru #}
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block page_title %}Документация{% endblock %}
|
{% block page_title %}Документация{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card readme-doc-card" aria-labelledby="readme-doc-heading">
|
<div class="readme-doc-shell" id="readme-doc-shell">
|
||||||
<div class="readme-doc-toolbar row spread">
|
<p id="readme-loading" class="muted readme-doc-loading">Загрузка…</p>
|
||||||
<h1 class="page-title" id="readme-doc-heading">Документация</h1>
|
|
||||||
<a href="/" class="nav-link nav-pill nav-pill--home">← Панель</a>
|
|
||||||
</div>
|
|
||||||
<p class="muted readme-doc-lead">
|
|
||||||
Текст загружается из <code>README.md</code> (эндпоинт <code>/api/v1/docs/readme</code>).
|
|
||||||
Разбор Markdown выполняется в браузере библиотеками из каталога <code>app/static/js/vendor/</code> (без внешних CDN).
|
|
||||||
</p>
|
|
||||||
<p id="readme-loading" class="muted readme-doc-loading">Загрузка документации…</p>
|
|
||||||
<p id="readme-error" class="msg hidden" role="alert"></p>
|
<p id="readme-error" class="msg hidden" role="alert"></p>
|
||||||
<article id="readme-doc-article" class="readme-doc markdown-body" hidden></article>
|
<div id="readme-doc-root" class="readme-doc-page" hidden></div>
|
||||||
</section>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Интерактивное создание ``.env`` в корне kind-k8s-develop.
|
"""Интерактивное создание ``.env`` в корне репозитория Kind Clusters Dashboard.
|
||||||
|
|
||||||
Список переменных и подсказок задаётся в этом файле (не используется env.example).
|
Список переменных и подсказок задаётся в этом файле (не используется env.example).
|
||||||
Дефолты совпадают с ``docker-compose.yml`` / приложением; Enter — записать предложенное значение.
|
Дефолты совпадают с ``docker-compose.yml`` / приложением; Enter — записать предложенное значение.
|
||||||
@@ -22,7 +22,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
logger = logging.getLogger("setup_env_interactive")
|
logger = logging.getLogger("setup_env_interactive")
|
||||||
|
|
||||||
# Корень репозитория kind-k8s-develop (родитель каталога scripts/)
|
# Корень репозитория Kind Clusters Dashboard (родитель каталога scripts/)
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
# Значения по умолчанию при нажатии Enter (как в docker-compose / Dockerfile / Settings).
|
# Значения по умолчанию при нажатии Enter (как в docker-compose / Dockerfile / Settings).
|
||||||
@@ -143,7 +143,7 @@ _SETUP_PROMPTS: tuple[_EnvPrompt, ...] = (
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
_ENV_FILE_HEADER = """# Файл .env для kind-k8s-develop
|
_ENV_FILE_HEADER = """# Файл .env для Kind Clusters Dashboard
|
||||||
# Создан интерактивно: scripts/setup_env_interactive.py
|
# Создан интерактивно: scripts/setup_env_interactive.py
|
||||||
#
|
#
|
||||||
# В docker-compose.yml заданы литералами (не из этого файла):
|
# В docker-compose.yml заданы литералами (не из этого файла):
|
||||||
@@ -259,7 +259,7 @@ def run(*, output: Path, force: bool = False) -> int:
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
_configure_logging()
|
_configure_logging()
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Интерактивное заполнение .env для kind-k8s-develop (переменные задаются в этом скрипте)",
|
description="Интерактивное заполнение .env для Kind Clusters Dashboard (переменные задаются в этом скрипте)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output",
|
"--output",
|
||||||
|
|||||||
Reference in New Issue
Block a user