# Описание 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-панель: единая карточка «панель + среда», статистика, создание кластера (прогресс, **журнал** `kind create`, отмена), таблица кластеров с **иконками** действий и **всплывающими подсказками**, модалка узлов/подов; шапка — пилюли, 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** показываются отдельными карточками (заголовок и тело). В шапке активна пилюля **Документация**. Путь к 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` на `` (по умолчанию `/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`**. Подробности — **README.md** (раздел «kubectl без установки на хост»). --- ## Сводка маршрутов 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` | Запуск: **200** — `docker start` узлов (кластер в kind); **202** + `job_id` — фоновый `kind create` по сохранённому `kind-config.yaml` | | POST | `/api/v1/clusters/{name}/stop` | Остановка узлов (`docker`/`podman` **stop**), запись в kind сохраняется | | 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` | Последние задания создания | | GET | `/api/v1/jobs/{job_id}` | Статус одного задания (включая `progress_stage`, `progress_percent`) | | POST | `/api/v1/jobs/{job_id}/cancel` | Запросить отмену создания (между этапами; `kind create` до конца не прерывается) | ### Фоновые задания (jobs) - Хранятся **только в памяти** процесса uvicorn; после перезапуска контейнера история обнуляется. - В памяти держится не более **200** записей; при превышении старые задания вытесняются (`app/core/job_store.py`). - Создание кластера: `POST /api/v1/clusters` → опрос `GET /api/v1/jobs/{job_id}` (как в веб-UI). - В ответе задания поля **`progress_stage`** (текст этапа) и **`progress_percent`** (0–100) обновляются во время создания. - Поле **`progress_log`** — массив последних строк журнала (вывод `kind create`: pull образов, подъём нод и т.д.); размер ограничен (см. `KIND_K8S_JOB_LOG_MAX_LINES` в коде `job_store`, по умолчанию до **500** строк в буфере, в JSON отдаётся хвост). - Тип задания **`kind`**: `create_cluster` или `start_cluster` (повторный подъём по `clusters/<имя>/kind-config.yaml`). - Статус **`cancelled`** — пользователь запросил отмену (`POST .../cancel`); этап `kind create cluster` до завершения не прерывается. --- ## 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 Сводная статистика для дашборда. **Пример ответа 200:** ```json { "kind_clusters_count": 2, "local_cluster_dirs_count": 2, "total_workers_from_meta": 4, "jobs_total": 5, "jobs_recent_failed": 1 } ``` - `total_workers_from_meta` может быть `null`, если ни в одном `meta.json` нет `worker_nodes`. - `jobs_total` — число заданий в текущей памяти процесса (не более 200). - `jobs_recent_failed` — сколько заданий в этом хранилище сейчас в статусе `failed` (не «последние N», а счётчик по всему снимку). --- ## 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 Список последних фоновых заданий (создание кластера), от новых к старым. **Query:** `limit` (1–200, по умолчанию **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 } ] ``` --- ## POST /api/v1/jobs/{job_id}/cancel Запрос отмены создания кластера. Пока задание в статусе `queued` или `running`, между этапами выполняется проверка флага; после уже запущенного `kind create cluster` нужно дождаться окончания этого шага. **Пример ответа 200:** ```json { "job_id": "a1b2…", "cancel_requested": true, "message": "Отмена обрабатывается между этапами; во время kind create дождитесь окончания шага" } ``` **Ошибка 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 Запуск кластера двумя сценариями: 1. Кластер **есть** в `kind get clusters` (узлы когда-либо создавались) — выполняется **`docker start`** / **`podman start`** для всех контейнеров с именами вида `<имя>-control-plane`, `<имя>-worker`, … Ответ **200**. 2. В **kind** кластера **нет**, но в `clusters/<имя>/kind-config.yaml` файл **есть** — ставится фоновое задание **`start_cluster`** (как при создании: `kind create` по сохранённому конфигу, журнал в `GET /jobs/{job_id}`). Ответ **202** + `job_id`. **Пример ответа 200 (контейнеры запущены):** ```json { "name": "dev", "mode": "containers", "containers_started_ok": true, "summary": "dev-control-plane: OK; dev-worker: OK; dev-worker2: OK" } ``` **Пример ответа 202 (подъём по конфигу):** ```json { "job_id": "cafebabe...", "status": "queued", "message": "Подъём кластера по kind-config.yaml; опросите GET /api/v1/jobs/{job_id}" } ``` **Ошибка 400:** некорректное имя или нет ни кластера в kind, ни `kind-config.yaml` в `clusters/<имя>/`. --- ## POST /api/v1/clusters/{name}/stop Остановка **всех** контейнеров узлов кластера (`docker stop` / `podman stop` по префиксу имени). Запись кластера в kind **не удаляется**; позже можно снова вызвать **POST …/start** (режим `containers`). **Пример ответа 200:** ```json { "name": "dev", "containers_stopped_ok": true, "summary": "dev-control-plane: OK; dev-worker: OK" } ``` **Ошибка 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": "kind create cluster (скачивание образов и подъём нод — может занять несколько минут)", "progress_percent": 28, "progress_log": [ "[12%] Подготовка каталога и kind-config", "--- kind create cluster ---", "Creating cluster \"dev\" ...", " • Ensuring node image (kindest/node:v1.29.4) 🖼 ..." ] } ``` **Успех (пример 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": ["[95%] Финализация", "kubectl wait nodes: ..."], "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): см. раздел «Веб-интерфейс и статика» выше.