Files
KindClustersDashboard/app/docs/api_routes.md
T
inecs d0933510b5 Журнал: пагинация 30 записей на страницу, API offset/total_pages
- job_journal: collect_recent_journal_entries_page_sync(limit, offset)
- GET /api/v1/journal/recent: limit по умолчанию 30, offset, total, page, total_pages
- journal.html/js: навигация Первая/Назад/номера/Вперёд/Последняя, стили
- app/docs/api_routes.md: описание query и пример ответа
- Прочие изменения UI/API (аддоны, helm, job_journal в кластерах) в том же коммите
2026-04-04 11:16:43 +03:00

919 lines
46 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-панель: единая карточка «панель + среда», статистика, **ссылка с имени кластера на** `GET /cluster/<имя>` (сводка ресурсов, kubectl, действия), **старт/стоп** кластера с тем же журналом (фоновые **POST …/start** и **…/stop**), модалка узлов/подов; шапка — пилюли, Swagger / ReDoc / Health в отдельных окнах. |
| `GET /cluster/{name}` | HTML **страница кластера**: донаты «Ресурсы узлов (сводка)», карточки узлов, **таблицы Kubernetes** (данные API кластера в JSON), кнопка **Рестарт** у подов (**`POST …/pods/restart`**), те же кнопки действий, что в таблице на главной; данные — **`GET /api/v1/clusters/{name}/overview`** (автообновление с интервалом панели). |
| `GET /cluster/{name}/edit` | HTML **редактирование** сохранённого `kind-config.yaml` и полей `meta.json` (простой режим: тег/workers; расширенный: полный YAML kind Cluster). Сохранение — **`PUT /api/v1/clusters/{name}/config`**. |
| `GET /cluster-addons` | HTML **Аддоны**: выбор кластера, **`GET /api/v1/helm/chart-versions`** для выпадающих списков версий чартов (или «Последняя»), установка/удаление Helm-релизов. |
| `GET /journal` | HTML **Журнал**: **`GET /api/v1/journal/recent`** с пагинацией (**30** записей на страницу), навигация по страницам внизу таблицы. |
| `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/cluster_detail.html` (страница кластера), `app/templates/cluster_edit.html` (редактирование конфигурации), `app/templates/cluster_addons.html` (Helm-аддоны), `app/templates/journal.html` (журнал заданий), `app/templates/documentation.html` (README).
**kubectl на хосте не обязателен:** бинарник есть в образе; узлы и поды доступны через API и веб-UI. Внутри контейнера веб-приложения `kubectl` использует временный kubeconfig с `server` через **`host.docker.internal:<порт>`** (см. `kubeconfig_patch.py`, `extra_hosts` в compose). Скачивание для хоста — **`GET …/kubeconfig`** (файл **`kubeconfig.host`** при наличии). Для консоли: **`make docker kubectl CLUSTER=<имя>`** — **`/work/clusters/<имя>/kubeconfig`**; при сбое попробуйте kubectl **с хоста** с **`clusters/<имя>/kubeconfig.host`**. Перезапуск веб-сервиса: **`make docker 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` | Список кластеров (поле `has_provision_log` — есть ли `clusters/<имя>/provision_log.json`) |
| POST | `/api/v1/clusters` | Создание в фоне (**202** + `job_id`) |
| POST | `/api/v1/clusters/{name}/start` | Запуск в фоне (**202** + `job_id`, поле `mode`: `containers`, `kind_config` или `kind_config_reapply`); журнал — `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}/config` | Текущие `meta.json` и текст `kind-config.yaml`; подсказка `kind_note` про пересоздание |
| PUT | `/api/v1/clusters/{name}/config` | Сохранить YAML и/или meta (простой режим или полный YAML); ответ включает обновлённый `summary` |
| GET | `/api/v1/clusters/{name}/kubeconfig` | Скачать файл kubeconfig |
| GET | `/api/v1/clusters/{name}/provision-log` | Полный журнал последнего **create_cluster** / **start_cluster** / **start_cluster_reapply** (JSON с диска) |
| GET | `/api/v1/clusters/{name}/workloads` | Узлы и поды (`kubectl`) |
| GET | `/api/v1/clusters/{name}/overview` | Сводка для страницы UI: метрики узлов, агрегаты, блоки **`k8s_*`** (JSON из `kubectl get … -o json`) |
| POST | `/api/v1/clusters/{name}/pods/restart` | Удалить под (`kubectl delete pod`) для мягкого рестарта |
| DELETE | `/api/v1/clusters/{name}` | Удалить кластер и данные в `clusters/` |
| GET | `/api/v1/clusters/{name}/addons/status` | Статус Helm-релизов (ingress-nginx, kube-prometheus-stack, metrics-server, Istio+Kiali); нужен **helm** в образе |
| GET | `/api/v1/helm/chart-versions` | Версии чартов для UI (после `helm repo update`); кэш **`KIND_K8S_HELM_VERSIONS_CACHE_SEC`**; поля: `ingress_nginx`, `kube_prometheus_stack`, `metrics_server`, `istio`, `kiali_server` |
| POST | `/api/v1/clusters/{name}/addons/ingress-nginx` | Установить **ingress-nginx** (NodePort 30080); тело: `{ "chart_version": "опционально" }` (пусто = последняя) |
| DELETE | `/api/v1/clusters/{name}/addons/ingress-nginx` | Удалить ingress-nginx |
| POST | `/api/v1/clusters/{name}/addons/kube-prometheus-stack` | Установить **kube-prometheus-stack**; тело: `grafana_admin_user`, `grafana_admin_password` (≥8), опционально **`chart_version`** |
| DELETE | `/api/v1/clusters/{name}/addons/kube-prometheus-stack` | Удалить стек |
| POST | `/api/v1/clusters/{name}/addons/metrics-server` | Установить **metrics-server** (kind: `--kubelet-insecure-tls`); тело опционально: `{ "chart_version": "…" }` или `{}` |
| DELETE | `/api/v1/clusters/{name}/addons/metrics-server` | Удалить metrics-server |
| POST | `/api/v1/clusters/{name}/addons/istio-kiali` | Установить **istio-base**, **istiod**, секрет и **kiali-server**; тело: `kiali_username`, `kiali_password` (≥8), опционально **`istio_chart_version`**, **`kiali_chart_version`** |
| DELETE | `/api/v1/clusters/{name}/addons/istio-kiali` | Удалить kiali-server, istiod, istio-base |
| GET | `/api/v1/journal/recent` | Пагинация журнала по всем кластерам: query **`limit`** (по умолчанию **30**, макс. 100), **`offset`** (по умолчанию **0**); в ответе **`total`**, **`total_pages`**, **`page`** |
| GET | `/api/v1/clusters/{name}/journal` | Полный файл журнала кластера (массив `entries` или пусто, если файла нет) |
| 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`).
- Снимок заданий сохраняется в JSON в каталоге **`clusters/`** (файл **`kind_k8s_jobs.json`** на томе с хоста) — после перезапуска контейнера список восстанавливается. Записи в статусе **queued**/**running** при старте помечаются как **failed** (процесс уже не выполняется). Путь переопределяется переменной **`KIND_K8S_JOBS_JSON`**.
- При **завершении** задания (успех / ошибка / отмена), если указано **`cluster_name`** и существует каталог **`clusters/<имя>/`**, в **`clusters/<имя>/journal/jobs_history.json`** дозаписывается запись с хвостом лога и **`result`** (без секретов). Лимиты: **`KIND_K8S_CLUSTER_JOURNAL_MAX_ENTRIES`**, **`KIND_K8S_CLUSTER_JOURNAL_MAX_LOG_LINES`**. Чтение: **`GET /api/v1/journal/recent`**, **`GET /api/v1/clusters/{name}/journal`**, страница **`GET /journal`**.
- Создание кластера: `POST /api/v1/clusters` → опрос `GET /api/v1/jobs/{job_id}` (как в веб-UI).
- В ответе задания поля **`progress_stage`** (текст этапа) и **`progress_percent`** (0–100) обновляются во время создания.
- В **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**); при переполнении старые строки вытесняются. Для **create_cluster**, **start_cluster** и **start_cluster_reapply** полный журнал без обрезки дополнительно сохраняется в **`clusters/<имя>/provision_log.json`** (перезапись при каждом завершении такого задания); чтение — **GET /api/v1/clusters/{name}/provision-log**.
- Для **`docker pull`**: если в справке **`docker pull --help`** объявлен флаг **`--progress`**, при **`KIND_K8S_DOCKER_PULL_PLAIN=1`** вызывается **`--progress=plain`** без PTY; на старых CLI флаг не передаётся (нет строки «unknown flag» в журнале). Для **podman** и **kind** — псевдо-TTY по `KIND_K8S_STREAM_PTY`, из строк убираются ANSI-коды.
- Тип задания **`kind`**: `create_cluster`, `start_cluster` (подъём по сохранённому конфигу), `start_cluster_reapply` (после правки `kind-config.yaml`: `kind delete` без удаления каталога + `kind create`), `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.
- Первым элементом всегда идёт **`latest`** (плавающий тег «самый новый образ»).
- Далее стабильные семверы **`vX.Y.Z`** без суффиксов (`-rc` и т.п.), **от новых к старым**, минимум **1.19.0**.
- Полный список семверов зависит от числа обходимых страниц API Hub: переменная **`KIND_K8S_HUB_TAGS_MAX_PAGES`** (по умолчанию в коде — 120 страниц, максимум при явной настройке — 500). Раньше UI обрезал список до 100 пунктов — сейчас отображаются все пришедшие теги.
При `KIND_K8S_SKIP_VERSION_LIST=1` список пустой.
**Пример ответа 200:**
```json
{
"tags": ["latest", "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`…). Поле **`container_cli`** совпадает с **`CONTAINER_CLI`** в среде приложения (`docker` или `podman`) — по нему UI показывает, что **все** узлы kind и сбор метрик идут через выбранный CLI.
**Пример ответа 200:**
```json
{
"container_cli": "docker",
"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,
"aggregate_cluster_resources": {
"nodes_count": 2,
"cpu_ring": 1.8,
"cpu_label": "ср. 1.8%",
"memory_percent_ring": 5.7,
"memory_percent_label": "ср. 5.7%",
"memory_used_ratio_ring": 5.8,
"memory_used_ratio_label": "ср. 5.8%",
"network_ring": 12.5,
"network_label": "всего 1.71 MiB",
"disk_ring": 45.0,
"disk_label": "всего 2.5 GiB"
}
}
```
- `container_cli` — активный движок для kind и `docker stats` / `podman stats` (как в GET /health).
- `total_workers_from_meta` — целое **≥ 0**; **0**, если ни в одном `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` может быть пустым, а `aggregate_cluster_resources` — нулевая сводка.
- `aggregate_cluster_resources` — агрегаты по **запущенным** узлам для донат-диаграмм на главной: средние проценты CPU/RAM, средняя доля RAM из строки `memory_usage`, кольца сети/диска по суммарному I/O (шкала 0–100 относительно порога 8 GiB на полное кольцо).
---
## GET /api/v1/clusters
Список: объединение `kind get clusters` и подкаталогов `clusters/*` (без дубликатов).
**Пример ответа 200 (массив):**
```json
[
{
"name": "dev",
"registered_in_kind": true,
"kind_nodes_running": true,
"has_local_kubeconfig": true,
"has_provision_log": 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
}
}
]
```
- `kind_nodes_running` — в списке процессов контейнерного движка есть узлы kind с префиксом имени (`имя-control-plane`, `имя-worker`…); для UI: при `true` показывается действие «Стоп», иначе при необходимости подъёма — «Старт».
- `has_provision_log` — в каталоге кластера есть файл **`provision_log.json`** (последнее завершённое задание создания или старта по сохранённому конфигу).
---
## GET /api/v1/helm/chart-versions
Список версий чартов для выпадающих списков на **`/cluster-addons`**. Выполняется **`helm repo update`** и для каждого чарта — **`helm search repo <chart> --versions -o json`**. Результаты кэшируются на **`KIND_K8S_HELM_VERSIONS_CACHE_SEC`** (по умолчанию **600** с) отдельно для каждого рефа чарта. Длина списка ограничена **`KIND_K8S_HELM_VERSIONS_MAX`** (по умолчанию **80**).
**Пример ответа 200:**
```json
{
"ingress_nginx": ["4.14.0", "4.13.3", "4.12.1"],
"kube_prometheus_stack": ["69.0.0", "68.4.3"],
"metrics_server": ["3.12.2", "3.12.1"],
"istio": ["1.24.3", "1.24.2"],
"kiali_server": ["2.1.0", "2.0.0"]
}
```
При недоступном **helm****503** с текстом ошибки.
---
## GET /api/v1/journal/recent
Объединение записей из всех **`clusters/*/journal/jobs_history.json`**, сортировка по **`finished_at_utc`** (новые первыми). В каждой записи добавляется **`source_cluster`** — имя каталога кластера.
**Query:**
| Параметр | Описание |
|----------|----------|
| `limit` | Записей на страницу, **1100**, по умолчанию **30** (как в веб-интерфейсе **`/journal`**) |
| `offset` | Смещение от начала списка, **≥ 0**, по умолчанию **0** (первая страница: `offset=0`, вторая при `limit=30`: `offset=30`) |
**Поля ответа:** **`limit`**, **`offset`**, **`total`** (всего записей), **`page`** (текущая страница с 1), **`total_pages`**, **`entries`** (фрагмент текущей страницы).
**Пример ответа 200 (первая страница, до 30 записей):**
```json
{
"limit": 30,
"offset": 0,
"total": 75,
"page": 1,
"total_pages": 3,
"entries": [
{
"job_id": "a1b2c3",
"kind": "create_cluster",
"cluster_name": "dev",
"source_cluster": "dev",
"status": "success",
"message": null,
"created_at_utc": "2026-04-04T11:58:00+00:00",
"finished_at_utc": "2026-04-04T12:05:00+00:00",
"log_lines": ["[1] …", "[2] …"],
"result": { "cluster_name": "dev" }
}
]
}
```
---
## GET /api/v1/clusters/{name}/journal
Содержимое файла **`clusters/<имя>/journal/jobs_history.json`**. Если файла нет — **`entries`: []**.
**Пример ответа 200:**
```json
{
"cluster_name": "dev",
"file_version": 1,
"entries": [
{
"version": 1,
"job_id": "a1b2c3",
"kind": "stop_containers",
"cluster_name": "dev",
"status": "success",
"message": null,
"created_at_utc": "2026-04-04T10:00:00+00:00",
"finished_at_utc": "2026-04-04T10:00:15+00:00",
"log_lines": [],
"result": null
}
]
}
```
---
## GET /api/v1/jobs
Список последних фоновых заданий, от новых к старым. Поле **`progress_log`** в каждом элементе **пустое** — используйте **GET /api/v1/jobs/{job_id}** для журнала.
**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,
"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_cluster_reapply`, `start_containers`, `stop_containers`). Для длительной команды завершается связанный дочерний процесс; между шагами запуска/остановки отдельных узлов также проверяется флаг отмены.
**Пример ответа 200:**
```json
{
"job_id": "a1b2…",
"cancel_requested": true,
"message": "Запрошено прерывание; текущая команда будет остановлена, задание перейдёт в отменено"
}
```
**Ошибка 400:** задание уже завершено.
**Ошибка 404:** неизвестный `job_id`.
---
## GET /api/v1/clusters/{name}/config
Чтение **`clusters/<имя>/meta.json`** и текста **`kind-config.yaml`**. Поле **`kind_note`** напоминает: kind **не** меняет топологию и образ уже созданного кластера — новый YAML на диске применится при следующем **`kind create`** (после `kind delete` и «Старт» / пересоздания).
**Ошибка 404:** каталога кластера нет.
**Ошибка 400:** некорректное имя.
**Пример ответа 200 (JSON):**
```json
{
"cluster_name": "dev",
"meta": {
"cluster_name": "dev",
"kubernetes_version_tag": "v1.29.4",
"worker_nodes": 2,
"node_image": "kindest/node:v1.29.4",
"description": "Тестовый стенд"
},
"kind_config_yaml": "kind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n- role: control-plane\n image: kindest/node:v1.29.4\n- role: worker\n image: kindest/node:v1.29.4\n",
"has_kind_config": true,
"registered_in_kind": true,
"kind_note": "Файлы сохранены на диске. Кластер уже зарегистрирован в kind: действующие узлы и образ не меняются до удаления кластера из kind и повторного создания по обновлённому kind-config.yaml (или «Старт», если запись в kind удалена, а каталог данных остался)."
}
```
---
## PUT /api/v1/clusters/{name}/config
Сохранение на диск. Нужно **хотя бы одно** поле тела:
| Поле | Назначение |
|------|------------|
| `kubernetes_version` | Тег `kindest/node` для **пересборки** простого YAML (control-plane + N worker) |
| `workers` | Число worker-нод 0–20 (вместе с версией из запроса или из meta) |
| `kind_config_yaml` | **Полная замена** `kind-config.yaml` (проверка: `kind: Cluster`, `apiVersion` с `kind.x-k8s.io`, `nodes`, есть `control-plane`) |
| `description` | Строка в meta (пустая строка **сбрасывает** поле) |
Если передан непустой **`kind_config_yaml`**, поля **`kubernetes_version`** и **`workers`** для генерации YAML **не используются** (можно не отправлять).
**Пример тела (только описание):**
```json
{
"description": "Обновлённая заметка"
}
```
**Пример тела (простой режим):**
```json
{
"kubernetes_version": "1.30.0",
"workers": 3,
"description": "Три воркера"
}
```
**Пример тела (расширенный YAML):**
```json
{
"kind_config_yaml": "kind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n- role: control-plane\n image: kindest/node:v1.29.4\n",
"description": ""
}
```
**Пример ответа 200 (JSON):**
```json
{
"cluster_name": "dev",
"updated_kind_config_yaml": true,
"message": "Сохранено.",
"registered_in_kind": true,
"kind_note": "Файлы сохранены на диске. Кластер уже зарегистрирован в kind: действующие узлы и образ не меняются до удаления кластера из kind и повторного создания по обновлённому kind-config.yaml (или «Старт», если запись в kind удалена, а каталог данных остался).",
"summary": {
"name": "dev",
"registered_in_kind": true,
"kind_nodes_running": true,
"has_local_kubeconfig": true,
"has_provision_log": true,
"meta": {
"kubernetes_version_tag": "v1.30.0",
"worker_nodes": 3
}
}
}
```
**Ошибка 404:** каталога кластера нет.
**Ошибка 400:** некорректное имя, пустое тело, ошибка разбора или структуры YAML.
---
## GET /api/v1/clusters/{name}/kubeconfig
Скачать kubeconfig для **kubectl на машине пользователя** (ответ — тело файла, `Content-Disposition`: `kubeconfig-{name}.yaml`).
При каждом запросе копируется **`clusters/{name}/kubeconfig`** и патчится `server=https://<KIND_K8S_KUBECONFIG_CLIENT_HOST или localhost>:<порт>` (порт из `docker port … 6443/tcp`). Если патч не удалился — отдаётся сохранённый **`kubeconfig.host`** или сырой **`kubeconfig`**.
**Ошибка 404:** нет ни `kubeconfig.host`, ни `kubeconfig` в `clusters/{name}/`.
**Ошибка 400:** некорректное имя кластера.
---
## GET /api/v1/clusters/{name}/provision-log
Содержимое **`clusters/<имя>/provision_log.json`**: полный журнал строк последнего завершённого задания **create_cluster**, **start_cluster** или **start_cluster_reapply** (включая успех, ошибку и отмену), плюс метаданные (`job_id`, `kind`, `status`, `message`, `result`, `finished_at_utc`).
**Ошибка 404:** файла нет (кластер ещё не создавали/не стартовали с сохранением журнала, или каталог без записи).
**Ошибка 400:** некорректное имя кластера.
**Пример ответа 200 (JSON):**
```json
{
"version": 1,
"job_id": "a1b2c3d4",
"kind": "create_cluster",
"cluster_name": "dev",
"finished_at_utc": "2026-04-04T14:30:00+00:00",
"status": "success",
"message": "Кластер создан",
"lines": [
"Creating cluster \"dev\" ...",
" • Ensuring node image (kindest/node:v1.29.4) 🖼"
],
"result": {
"cluster_name": "dev",
"kubernetes_version_tag": "v1.29.4",
"kubeconfig_path": "/work/clusters/dev/kubeconfig"
}
}
```
---
## 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`.
Если кластер **есть в kind**, но **узлы остановлены** (`docker stop` / «Стоп» в UI): **`error`** — краткое сообщение без вызова kubectl (вместо сырого stderr про DNS `*-control-plane`).
**Ошибка 400:** некорректное имя кластера.
---
## GET /api/v1/clusters/{name}/overview
Единый ответ для **страницы кластера** (`GET /cluster/{name}`): флаги и `meta` как в списке кластеров, **метрики узлов** (docker/podman), **`aggregate_resources`** (как в `GET /stats` для донатов), данные Kubernetes в виде **`kubectl get … -o json`** (разбор `items` на фронтенде в таблицы).
- При отсутствии kubeconfig: **`kubeconfig_error`** — строка-пояснение; блоки **`k8s_*`** недоступны (см. ниже).
- Если кластер **зарегистрирован в kind**, но **узлы не запущены** (`kind_nodes_running: false`): **`kubectl` не вызывается**; в **`kubeconfig_error`** и в **`message`** каждого блока **`k8s_*`** — одно и то же дружелюбное пояснение (без stderr про недоступный API).
- Поля **`nodes_rc` / `nodes_output`**, **`pods_rc` / `pods_output`**, … — устаревший текстовый вывод; в текущей версии API могут быть **`null`** (UI использует только JSON-блоки).
- Блоки **`k8s_nodes`**, **`k8s_namespaces`**, **`k8s_pods`**, **`k8s_deployments`**, **`k8s_statefulsets`**, **`k8s_daemonsets`**, **`k8s_replicasets`**, **`k8s_jobs`**, **`k8s_cronjobs`**, **`k8s_services`**, **`k8s_ingresses`** (ресурс **`ingresses.networking.k8s.io -A`**), **`k8s_pvcs`** — объекты вида **`K8sListJsonBlock`**; для ресурсов в namespace везде **`kubectl get … -A`** (все пространства имён).
- **`ok`**: `true` при успешном `kubectl`;
- **`rc`**: код возврата;
- **`items`**: массив объектов из `kubectl` (как в API Kubernetes);
- **`message`**: текст ошибки при `ok: false`.
- **`resources_error`** — ошибка сбора метрик контейнерного CLI (как `cluster_resources_error` в `/stats`).
- **`cluster_resources`** — один блок `KindClusterResources` (узлы с полями `cpu_percent`, `memory_usage`, …).
**Пример ответа 200 (фрагмент):**
```json
{
"cluster_name": "dev",
"registered_in_kind": true,
"kind_nodes_running": true,
"has_local_kubeconfig": true,
"has_provision_log": true,
"meta": { "worker_nodes": 2, "kubernetes_version_tag": "v1.29.4" },
"resources_error": null,
"cluster_resources": {
"cluster_name": "dev",
"nodes": [
{
"container_name": "dev-control-plane",
"cpu_percent": "2.10%",
"memory_usage": "800MiB / 7.7GiB",
"memory_percent": "10.14%",
"net_io": "1.2MB / 800kB",
"block_io": "0B / 0B",
"pids": 120
}
],
"note": null
},
"aggregate_resources": {
"nodes_count": 3,
"cpu_ring": 5.2,
"cpu_label": "5.2%",
"memory_percent_ring": 12.0,
"memory_percent_label": "12%",
"memory_used_ratio_ring": 45.0,
"memory_used_ratio_label": "2.1 / 4.6 GiB",
"network_ring": 8.0,
"network_label": "↓ 10 MB",
"disk_ring": 3.0,
"disk_label": "R/W 2 MB"
},
"kubeconfig_error": null,
"k8s_nodes": {
"ok": true,
"rc": 0,
"items": [{ "metadata": { "name": "dev-control-plane" }, "status": { "conditions": [] } }],
"message": null
},
"k8s_pods": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_deployments": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_statefulsets": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_daemonsets": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_services": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_ingresses": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_namespaces": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_replicasets": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_jobs": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_cronjobs": { "ok": true, "rc": 0, "items": [], "message": null },
"k8s_pvcs": { "ok": true, "rc": 0, "items": [], "message": null }
}
```
**Ошибка 400:** некорректное имя кластера.
---
## POST /api/v1/clusters/{name}/pods/restart
Мягкий **рестарт пода**: выполняется **`kubectl delete pod`** в указанном namespace (под пересоздаётся, если им управляет Deployment/ReplicaSet и т.д.).
**Тело запроса (JSON):**
```json
{
"namespace": "default",
"pod": "my-app-7d4f8b9-xk2cp"
}
```
**Пример ответа 200:**
```json
{
"ok": true,
"cluster_name": "dev",
"namespace": "default",
"pod": "my-app-7d4f8b9-xk2cp",
"message": null
}
```
**Ошибка 400:** неверное имя кластера, namespace или pod (валидация как в Kubernetes).
**Ошибка 502:** `kubectl delete` завершился с ненулевым кодом (текст stderr в теле ответа от сервера).
---
## 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 и в **`meta.json`** **нет** флага **`apply_kind_config_on_next_start`** — задание **`start_containers`** (поочерёдный `docker start` узлов, `mode`: **`containers`**).
2. Кластер **зарегистрирован** в kind и после **PUT …/config** с изменением **`kind-config.yaml`** в **`meta.json`** выставлен **`apply_kind_config_on_next_start`: true** — задание **`start_cluster_reapply`** (`mode`: **`kind_config_reapply`**): `kind delete cluster` **без** удаления `clusters/<имя>/`, затем **`kind create`** по сохранённому YAML (как при создании; флаг в новом `meta.json` не сохраняется).
3. В kind кластера **нет**, но есть **`clusters/<имя>/kind-config.yaml`** — задание **`start_cluster`** (`mode`: **`kind_config`**).
Остановка (**POST …/stop**) флаг пересоздания **не сбрасывает**; достаточно после правки конфига нажать **«Старт»** (после стопа или сразу — выполнится сценарий 2, если кластер всё ещё в kind).
**Пример ответа 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}"
}
```
**Пример ответа 202 (пересоздание после правки YAML):**
```json
{
"job_id": "c0ffee...",
"status": "queued",
"mode": "kind_config_reapply",
"message": "Удаление записи в kind и создание кластера по обновлённому kind-config.yaml; GET /api/v1/jobs/{job_id}"
}
```
**Ошибка 400:** некорректное имя; или нет ни кластера в kind, ни `kind-config.yaml`; или для **`kind_config_reapply`** отсутствует `kind-config.yaml`.
---
## 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): см. раздел «Веб-интерфейс и статика» выше.