Files
KindClustersDashboard/app/docs/api_routes.md
Sergey Antropoff aa8003061e Документация: restart в Makefile, актуальное описание UI и API
- README: make docker/podman restart, старт/стоп, журнал, отмена, очистка заданий
- api_routes.md: панель, DELETE /jobs, ссылка на make restart
- Makefile: цель restart (если ещё не в origin)
2026-04-04 07:06:03 +03:00

523 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Описание REST API веб-интерфейса Kind Clusters Dashboard
**Базовый префикс:** `/api/v1`
**Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)
## Как смотреть документацию
| Способ | URL / путь |
|--------|------------|
| Swagger UI (OpenAPI) | `http://127.0.0.1:<порт>/docs` (порт на хосте по умолчанию **8080**, см. `KIND_K8S_WEB_PORT`; 6000 на хосте блокируется Chrome) |
| ReDoc | `http://127.0.0.1:<порт>/redoc` |
| Health (JSON) | `http://127.0.0.1:<порт>/api/v1/health` |
| Документация проекта | `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` в репозитории |
С **веб-панели** (`GET /`) пункты меню **Swagger**, **ReDoc** и **Health** вызывают `window.open` с именами окон `kind_swagger`, `kind_redoc`, `kind_health` (отдельное окно, повторный клик переиспользует то же окно). Пункт **Документация** открывает `GET /documentation` в той же вкладке.
## Веб-интерфейс и статика (не JSON)
| Маршрут | Описание |
|---------|----------|
| `GET /` | HTML-панель: единая карточка «панель + среда», статистика, создание кластера (прогресс, **журнал** в реальном времени, **«Отменить»** прерывает текущую команду), **старт/стоп** кластера с тем же журналом (фоновые **POST …/start** и **…/stop**), таблица **последних заданий** с кнопкой **«Очистить завершённые»** (**DELETE /api/v1/jobs**), модалка узлов/подов; шапка — пилюли, Swagger / ReDoc / Health в отдельных окнах. |
| `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****одна карточка** (заголовок h2 и содержимое до следующего h2 вместе). Заголовок вкладки браузера: **«Документация — …»** + текст **первого H1** документа + имя приложения (`KIND_K8S_APP_TITLE` на `body`). В шапке на этой странице активна только **Документация**; **Панель** как обычная пилюля (на дашборде активна **Панель**). Путь к README: `KIND_K8S_README_PATH` или `README.md` рядом с `app/`; в образе — `/opt/kind-k8s/README.md`. |
| `GET /ui` | Редирект **307** на `/` (удобный ярлык). |
| `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).
**kubectl на хосте не обязателен:** бинарник есть в образе; узлы и поды доступны через API (**`GET /api/v1/clusters/{name}/workloads`**) и веб-UI. Для интерактивной консоли из корня репозитория при запущенном compose: **`make docker kubectl CLUSTER=<имя>`** (или **`make podman kubectl …`**), внутри контейнера kubeconfig — **`/work/clusters/<имя>/kubeconfig`**. Перезапуск только веб-сервиса без `down`/`up`: **`make docker restart`** / **`make podman restart`**. Подробности — **README.md**.
---
## Сводка маршрутов API
| Метод | Путь | Кратко |
|-------|------|--------|
| GET | `/api/v1/health` | Среда: kind, kubectl, движок контейнеров |
| 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/stats` | Сводка для дашборда |
| GET | `/api/v1/clusters` | Список кластеров |
| POST | `/api/v1/clusters` | Создание в фоне (**202** + `job_id`) |
| POST | `/api/v1/clusters/{name}/start` | Запуск в фоне (**202** + `job_id`, поле `mode`: `containers` или `kind_config`); журнал — `GET /jobs/{job_id}` |
| POST | `/api/v1/clusters/{name}/stop` | Остановка узлов в фоне (**202** + `job_id`, `mode`: `stop`); журнал — `GET /jobs/{job_id}` |
| GET | `/api/v1/clusters/{name}` | Детали + `kubectl get nodes` при наличии kubeconfig |
| GET | `/api/v1/clusters/{name}/kubeconfig` | Скачать файл kubeconfig |
| GET | `/api/v1/clusters/{name}/workloads` | Узлы и поды (`kubectl`) |
| DELETE | `/api/v1/clusters/{name}` | Удалить кластер и данные в `clusters/` |
| GET | `/api/v1/jobs` | Последние задания (`progress_log` в ответе пустой — полный журнал только в GET по `job_id`) |
| GET | `/api/v1/jobs/{job_id}` | Статус задания + полный хвост `progress_log` (лимит см. `KIND_K8S_JOB_API_LOG_MAX_LINES`) |
| DELETE | `/api/v1/jobs` | Удалить из памяти **завершённые** задания (`removed` — число записей) |
| POST | `/api/v1/jobs/{job_id}/cancel` | Прервать задание: активная команда завершается принудительно (скачивание образа, создание кластера, старт/стоп узла) |
### Фоновые задания (jobs)
- Хранятся **только в памяти** процесса uvicorn; после перезапуска контейнера история обнуляется.
- В памяти держится не более **200** записей; при превышении старые задания вытесняются (`app/core/job_store.py`).
- Создание кластера: `POST /api/v1/clusters` → опрос `GET /api/v1/jobs/{job_id}` (как в веб-UI).
- В ответе задания поля **`progress_stage`** (текст этапа) и **`progress_percent`** (0100) обновляются во время создания.
- В **GET /api/v1/jobs** (список) поле **`progress_log`** всегда **пустой массив** — меньше трафика; полный хвост — в **GET /api/v1/jobs/{job_id}** (лимит строк: `KIND_K8S_JOB_API_LOG_MAX_LINES`, по умолчанию **5000**).
- Буфер строк в памяти на задание: `KIND_K8S_JOB_LOG_MAX_LINES` (по умолчанию **2500**); при переполнении старые строки вытесняются.
- Для **`docker pull`** по умолчанию: **`--progress=plain`** и вывод **без PTY** (`KIND_K8S_DOCKER_PULL_PLAIN=1`) — в журнале идут строки с процентами/слоями. Для **podman** и **kind** — псевдо-TTY по `KIND_K8S_STREAM_PTY`, из строк убираются ANSI-коды.
- Тип задания **`kind`**: `create_cluster`, `start_cluster` (подъём по сохранённому конфигу), `start_containers` (запуск уже созданных узлов), `stop_containers` (остановка узлов).
- Статус **`cancelled`** — запрошена отмена (`POST .../cancel`); дочерний процесс текущей команды получает принудительное завершение.
---
## GET /api/v1/health
Проверка: `kind`/`kubectl` в PATH и ответ движка контейнеров (`docker info` / `podman info` по `CONTAINER_CLI`).
`status`: `ok` — всё готово к созданию кластеров; `degraded` — чего-то не хватает (см. поля ниже).
**Пример ответа 200 (JSON):**
```json
{
"status": "ok",
"kind_in_path": true,
"kubectl_in_path": true,
"container_cli": "docker",
"container_engine_ok": true,
"container_engine_detail": null
}
```
**Если сокет Docker недоступен:**
```json
{
"status": "degraded",
"kind_in_path": true,
"kubectl_in_path": true,
"container_cli": "docker",
"container_engine_ok": false,
"container_engine_detail": "Cannot connect to the Docker daemon..."
}
```
---
## GET /api/v1/docs/readme
Сырое содержимое **README.md** проекта в кодировке UTF-8, заголовок **`Content-Type: text/markdown; charset=utf-8`**.
Используется страницей **`GET /documentation`**: скрипт `documentation.js` загружает текст и превращает его в HTML через **marked** и **DOMPurify** (файлы лежат в репозитории: `app/static/js/vendor/`, без внешних CDN).
**Ошибка 404:** файл не найден. В Compose смонтируйте `./README.md:/opt/kind-k8s/README.md:ro`, задайте `KIND_K8S_README_PATH` или пересоберите образ (`COPY README.md`). См. `app/core/readme_doc.py`.
---
## 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
Список стабильных тегов `kindest/node` с Docker Hub (для выпадающего списка в UI).
При `KIND_K8S_SKIP_VERSION_LIST=1` список пустой.
**Пример ответа 200:**
```json
{
"tags": ["v1.32.0", "v1.31.4"],
"skipped": false
}
```
**Пример при пропуске загрузки:**
```json
{
"tags": [],
"skipped": true,
"reason": "KIND_K8S_SKIP_VERSION_LIST"
}
```
---
## GET /api/v1/stats
Сводная статистика для дашборда и **метрики запущенных узлов kind** (один вызов `docker stats` / `podman stats` на набор имён контейнеров `имя-control-plane`, `имя-worker`…).
**Пример ответа 200:**
```json
{
"kind_clusters_count": 1,
"local_cluster_dirs_count": 1,
"total_workers_from_meta": 2,
"jobs_total": 3,
"jobs_recent_failed": 0,
"cluster_resources": [
{
"cluster_name": "dev",
"nodes": [
{
"container_name": "dev-control-plane",
"cpu_percent": "2.50%",
"memory_usage": "512MiB / 7.7GiB",
"memory_percent": "6.50%",
"net_io": "1.2MB / 800kB",
"block_io": "2GB / 150MB",
"pids": 245
},
{
"container_name": "dev-worker",
"cpu_percent": "1.00%",
"memory_usage": "380MiB / 7.7GiB",
"memory_percent": "4.80%",
"net_io": "512kB / 400kB",
"block_io": "500MB / 50MB",
"pids": 198
}
],
"note": null
}
],
"cluster_resources_error": null
}
```
- `total_workers_from_meta` может быть `null`, если ни в одном `meta.json` нет `worker_nodes`.
- `jobs_total` — число заданий в текущей памяти процесса (не более 200).
- `jobs_recent_failed` — сколько заданий в этом хранилище сейчас в статусе `failed` (не «последние N», а счётчик по всему снимку).
- `cluster_resources` — по каждому имени из `kind get clusters`; если узлы остановлены, `nodes` пустой, в `note` пояснение.
- `cluster_resources_error` — если CLI (`CONTAINER_CLI`) не найден в PATH и т.п.; тогда `cluster_resources` может быть пустым.
---
## GET /api/v1/clusters
Список: объединение `kind get clusters` и подкаталогов `clusters/*` (без дубликатов).
**Пример ответа 200 (массив):**
```json
[
{
"name": "dev",
"registered_in_kind": true,
"has_local_kubeconfig": true,
"meta": {
"cluster_name": "dev",
"kubernetes_version_tag": "v1.29.4",
"node_image": "kindest/node:v1.29.4",
"worker_nodes": 2,
"kubeconfig_patched_for_host": true
}
}
]
```
---
## GET /api/v1/jobs
Список последних фоновых заданий, от новых к старым. Поле **`progress_log`** в каждом элементе **пустое** — используйте **GET /api/v1/jobs/{job_id}** для журнала.
**Query:** `limit` (1200, по умолчанию **30**).
**Пример ответа 200 (массив `JobView`):**
```json
[
{
"job_id": "abc123",
"kind": "create_cluster",
"status": "success",
"cluster_name": "dev",
"created_at_utc": "2026-04-04T12:00:00+00:00",
"message": "Кластер создан",
"result": { "cluster_name": "dev", "kubernetes_version_tag": "v1.29.4" },
"progress_stage": null,
"progress_percent": null,
"progress_log": []
}
]
```
---
## DELETE /api/v1/jobs
Удаляет из памяти записи заданий со статусом **success**, **failed** или **cancelled**. Задания **queued** и **running** не удаляются.
**Пример ответа 200:**
```json
{
"removed": 4
}
```
---
## POST /api/v1/jobs/{job_id}/cancel
Прервать фоновое задание (`create_cluster`, `start_cluster`, `start_containers`, `stop_containers`). Для длительной команды завершается связанный дочерний процесс; между шагами запуска/остановки отдельных узлов также проверяется флаг отмены.
**Пример ответа 200:**
```json
{
"job_id": "a1b2…",
"cancel_requested": true,
"message": "Запрошено прерывание; текущая команда будет остановлена, задание перейдёт в отменено"
}
```
**Ошибка 400:** задание уже завершено.
**Ошибка 404:** неизвестный `job_id`.
---
## GET /api/v1/clusters/{name}/kubeconfig
Скачать файл `kubeconfig` (ответ — тело файла, `Content-Disposition` с именем `kubeconfig-{name}.yaml`).
**Ошибка 404:** файла нет в `clusters/{name}/`.
**Ошибка 400:** некорректное имя кластера.
---
## GET /api/v1/clusters/{name}/workloads
`kubectl get nodes -o wide` и `kubectl get pods -A` по сохранённому kubeconfig.
**Пример ответа 200:**
```json
{
"cluster_name": "dev",
"nodes_rc": 0,
"nodes_output": "NAME STATUS ROLES ...",
"pods_rc": 0,
"pods_output": "NAMESPACE NAME READY STATUS ...",
"error": null
}
```
Если kubeconfig нет: `error` — строка вида **`Нет сохранённого kubeconfig в clusters/<имя>/`**, поля вывода kubectl могут быть `null`.
**Ошибка 400:** некорректное имя кластера.
---
## GET /api/v1/clusters/{name}
Детали и попытка `kubectl get nodes -o wide` с **сохранённого** `clusters/{name}/kubeconfig` (если файл есть).
**Пример ответа 200:**
```json
{
"name": "dev",
"registered_in_kind": true,
"has_local_kubeconfig": true,
"kubeconfig_path": "/work/clusters/dev/kubeconfig",
"meta": { "worker_nodes": 2 },
"kubectl_get_nodes_rc": 0,
"kubectl_get_nodes": "NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME\n..."
}
```
---
## POST /api/v1/clusters
Создание кластера **в фоне** (ответ **202**).
**Тело запроса (JSON):**
```json
{
"name": "dev",
"kubernetes_version": "1.29.4",
"workers": 2
}
```
**Пример ответа 202:**
```json
{
"job_id": "a1b2c3d4e5f6...",
"status": "queued",
"message": "Создание кластера выполняется в фоне; опросите GET /api/v1/jobs/{job_id}"
}
```
**Ошибка 400:** невалидное имя кластера или тело не проходит валидацию Pydantic.
**Ошибка 409 (кластер уже есть в kind):**
```json
{
"detail": "Кластер с таким именем уже есть в kind"
}
```
---
## POST /api/v1/clusters/{name}/start
Запуск кластера двумя сценариями (оба с **202** и `job_id`, журнал в `GET /jobs/{job_id}`):
1. Кластер **зарегистрирован** в kind — задание **`start_containers`** (поочерёдный запуск узлов, `mode`: **`containers`**).
2. В kind кластера **нет**, но есть сохранённый **`clusters/<имя>/kind-config.yaml`** — задание **`start_cluster`** (`mode`: **`kind_config`**), логика как при создании (в т.ч. скачивание образа при необходимости).
**Пример ответа 202 (запуск узлов):**
```json
{
"job_id": "cafebabe...",
"status": "queued",
"mode": "containers",
"message": "Запуск узлов; опросите GET /api/v1/jobs/{job_id}"
}
```
**Пример ответа 202 (подъём по конфигу):**
```json
{
"job_id": "deadbeef...",
"status": "queued",
"mode": "kind_config",
"message": "Подъём кластера по сохранённому конфигу; опросите GET /api/v1/jobs/{job_id}"
}
```
**Ошибка 400:** некорректное имя или нет ни кластера в kind, ни `kind-config.yaml` в `clusters/<имя>/`.
---
## POST /api/v1/clusters/{name}/stop
Остановка узлов кластера **в фоне** (задание **`stop_containers`**). Запись кластера в kind **не удаляется**; позже снова **POST …/start**.
**Пример ответа 202:**
```json
{
"job_id": "baba...",
"status": "queued",
"mode": "stop",
"message": "Остановка узлов; опросите GET /api/v1/jobs/{job_id}"
}
```
**Ошибка 400:** некорректное имя кластера.
---
## GET /api/v1/jobs/{job_id}
Статус фонового задания создания.
**В процессе (пример 200):**
```json
{
"job_id": "a1b2...",
"kind": "create_cluster",
"status": "running",
"cluster_name": "dev",
"created_at_utc": "2026-04-04T12:00:00+00:00",
"message": null,
"result": null,
"progress_stage": "Создание узлов кластера",
"progress_percent": 45,
"progress_log": [
"Подготовка конфигурации",
"Using default tag: latest",
"Status: Downloaded newer image for kindest/node:v1.29.4",
"Creating cluster \"dev\" ..."
]
}
```
**Успех (пример 200):**
```json
{
"job_id": "a1b2...",
"kind": "create_cluster",
"status": "success",
"cluster_name": "dev",
"created_at_utc": "2026-04-04T12:00:00+00:00",
"message": "Кластер создан",
"progress_log": ["Завершение", "Узлы готовы: condition met"],
"result": {
"cluster_name": "dev",
"kubernetes_version_tag": "v1.29.4",
"node_image": "kindest/node:v1.29.4",
"workers": 2,
"kubeconfig_path": "/work/clusters/dev/kubeconfig",
"kubeconfig_patched_for_host": true,
"nodes_ready": true,
"nodes_ready_message": "..."
}
}
```
**Ошибка 404:**
```json
{
"detail": "Задание не найдено"
}
```
---
## DELETE /api/v1/clusters/{name}
`kind delete cluster` и удаление каталога `clusters/{name}/`.
**Пример ответа 200:**
```json
{
"name": "dev",
"kind_delete_ok": true,
"summary": "kind delete: OK; удалена папка /work/clusters/dev"
}
```
**Ошибка 400:** некорректное имя кластера.
**Ошибка 500:** логическая ошибка удаления (тело с `detail`).
---
## GET /
HTML-дашборд (не JSON): см. раздел «Веб-интерфейс и статика» выше.