# Описание REST API веб-интерфейса Kind Clusters Dashboard **Базовый префикс:** `/api/v1` ## Как смотреть документацию | Способ | 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 **Панель**: CTA **«Перейти к созданию кластера»**, карточка **Статистика** (среда kind/kubectl, счётчики), отдельная карточка **Ресурсы узлов (сводка)** (донаты по **`GET /api/v1/stats`**); полная таблица кластеров — на **`GET /clusters`**. Полноэкранный спиннер первой загрузки — как у **`/cluster-create`** и **`/documentation`**. | | `GET /cluster-create` | HTML **Создание кластера**: форма, прогресс и журнал задания, таблица **«Последние задания»** (журнал с диска, инкрементальное обновление без сброса раскрытого лога); **`dashboard.js`**. Спиннер первой загрузки. | | `GET /clusters` | HTML **Кластеры**: шапка с кнопкой **Создать кластер** (`/cluster-create`), сводка **Ресурсы узлов**, таблица кластеров (**старт/стоп**, ссылка на `GET /cluster/<имя>`, модалки как на панели); скрипт **`dashboard.js`**. Спиннер первой загрузки. | | `GET /cluster/{name}` | HTML **страница кластера**: донаты «Ресурсы узлов (сводка)», карточки **Ресурсы узлов**, затем отдельная карточка **Установленные аддоны** (**мини-карточки Helm**, **`GET /api/v1/clusters/{name}/addons/status`**, ссылки на **`/cluster-addons`**), **таблицы Kubernetes** (данные API кластера в JSON), кнопка **Рестарт** у подов (**`POST …/pods/restart`**), те же кнопки действий, что в таблице на главной; данные — **`GET /api/v1/clusters/{name}/overview`** (автообновление с интервалом панели). В шапке активна пилюля **Кластеры** (`nav_active: clusters`). | | `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 /helm/chart-versions`** + **`GET …/addons/status`**; для **установленных** релизов — **`GET …/addons/installed-values`**: в селекте версия с пометкой «(текущая установленная версия)», в форме — values из кластера. По кнопке **«Загрузить values»** — шаблон из **`POST /helm/addons/compose-values`**. Установка/удаление релизов. | | `GET /journal` | HTML **Журналы**: переключатель (как «Простой/Расширенный» в редактировании кластера) — **по кластеру** (`journal/recent?cluster=`), **развёртывание** (`/journal/provision`), **Helm-аддоны** (`/journal/helm-addons`); пагинация по **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). Полноэкранный спиннер при первой загрузке и при **каждом** переходе по внутренним ссылкам / **popstate**. Каждая секция по **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` на `` (по умолчанию `/api/v1`). | | `GET /static/docs-images/…` | PNG из **`app/docs/images/`** для страницы **`/documentation`** (в Markdown пути вида `images/*.png` в **`documentation.js`** переписываются на этот префикс; см. **`app/docs/screenshots.md`**). | Шаблоны: `app/templates/base.html` (шапка, навигация; **меню «гамбургер»** при узком viewport — `nav-mobile.js`), `app/templates/dashboard.html` (панель), `app/templates/clusters.html` (список кластеров и донаты узлов), `app/templates/cluster_create.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 и `app/docs/*.md`). **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}/kubeconfig/docker` | Скачать kubeconfig для kubectl из **любого** контейнера: `host.docker.internal:<порт>` + `tls-server-name` (запасной вариант — `*-control-plane:6443`) | | 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-релизов + **версии чартов** из `helm list` (`ingress_nginx_chart_version`, `kube_prometheus_stack_chart_version`, `metrics_server_chart_version`, `istiod_chart_version`, `istio_base_chart_version`, `kiali_server_chart_version`), если релиз установлен; нужен **helm** | | GET | `/api/v1/clusters/{name}/addons/installed-values` | Для **установленных** аддонов: эффективный YAML (`helm get values --all -o yaml`) и версии; поля `ingress_nginx`, `kube_prometheus_stack`, `metrics_server`, `istio_kiali` (или `null`); лимит размера YAML в ответе | | 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**; тело: опционально `chart_version`, опционально **`values_yaml`** (полный YAML чарта, см. **compose-values** и раздел ниже) | | 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`, опционально **`values_yaml`** (полный YAML чарта; пусто — автосборка) | | DELETE | `/api/v1/clusters/{name}/addons/kube-prometheus-stack` | Удалить стек | | POST | `/api/v1/clusters/{name}/addons/metrics-server` | Установить **metrics-server**; тело опционально: `chart_version`, **`values_yaml`** (полный YAML; пусто — автосборка с args для kind) | | DELETE | `/api/v1/clusters/{name}/addons/metrics-server` | Удалить metrics-server | | POST | `/api/v1/clusters/{name}/addons/istio-kiali` | Установить **istio-base**, **istiod**, секрет и **kiali-server**; тело: учётные данные Kiali, версии, опционально **`values_yaml`** (kiali-server), **`istio_base_values_yaml`**, **`istio_istiod_values_yaml`** | | DELETE | `/api/v1/clusters/{name}/addons/istio-kiali` | Удалить kiali-server, istiod, istio-base | | POST | `/api/v1/helm/addons/compose-values` | Собрать YAML для **одного** аддона (тело: `addon`, версии, поля Grafana при необходимости) | | POST | `/api/v1/helm/addons/compose-values-batch` | Собрать **все** шесть YAML для страницы аддонов **одним** запросом (один **`helm repo update`**); предпочтительно для UI | | GET | `/api/v1/journal/recent` | Журнал заданий из **`journal/jobs_history.json`**: query **`limit`**, **`offset`**, опционально **`cluster`** (только выбранный каталог); ответ **`total`**, **`total_pages`**, **`page`** | | GET | `/api/v1/journal/provision` | Сводка **`provision_log.json`** по кластерам (по одному последнему файлу на каталог), пагинация | | GET | `/api/v1/journal/helm-addons` | Сводка **`helm_addon_log.json`**: все записи из **`entries`** по кластерам (история), тот же формат строки, что у provision | | GET | `/api/v1/clusters/{name}/journal` | Полный файл **`jobs_history.json`** кластера (массив `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` | Прервать задание: активная команда завершается принудительно (скачивание образа, создание кластера, старт/стоп узла) | После каждого **POST/DELETE …/addons/…** в каталоге кластера в **`helm_addon_log.json`** **добавляется** новая запись в массив **`entries`** (история сохраняется, лимит **`KIND_K8S_HELM_ADDON_LOG_MAX_ENTRIES`**). Формат записи как у **`provision_log.json`**: `lines`, `kind`, `status`, **`message`** (краткая подпись на русском при успехе), **`result`**; в **`result`** поля **`addon`** и **`action`**: **`install`** | **`upgrade`** (повторный POST при уже установленном релизе) | **`delete`**; пароли в файл не пишутся. Поле **`kind`**: `helm_addon_install_*`, `helm_addon_upgrade_*`, `helm_addon_delete_*` (суффикс — аддон в snake_case). #### POST `/api/v1/helm/addons/compose-values-batch` Тело **`HelmAddonComposeBatchRequest`**: версии чартов из селектов (`ingress_chart_version`, `kube_prometheus_chart_version`, `metrics_server_chart_version`, `istio_chart_version`, `kiali_chart_version`) и опционально **`grafana_admin_user`** / **`grafana_admin_password`**. Ответ **`HelmAddonComposeBatchResponse`**: поля **`ingress_nginx_values_yaml`**, **`kube_prometheus_values_yaml`**, **`metrics_server_values_yaml`**, **`kiali_values_yaml`**, **`istio_base_values_yaml`**, **`istio_istiod_values_yaml`**. #### POST `/api/v1/helm/addons/compose-values` Тело JSON (**`HelmAddonComposeValuesRequest`**): - **`addon`**: `ingress-nginx` | `kube-prometheus-stack` | `metrics-server` | `istio-kiali` - **`chart_version`** — для ingress / kube-prometheus-stack / metrics-server (пусто = последняя версия в `helm search`) - **`grafana_admin_user`**, **`grafana_admin_password`** — для `kube-prometheus-stack` (если пароль пуст, в preview подставляется временная строка **`ChangeMe12345`** — замените в YAML или в форме перед установкой) - **`istio_chart_version`**, **`kiali_chart_version`** — для `istio-kiali` Ответ (**`HelmAddonComposeValuesResponse`**): поле **`values_yaml`** — полный текст для основного чарта; для **`istio-kiali`** дополнительно **`istio_base_values_yaml`** и **`istio_istiod_values_yaml`**. Выполняется **`helm repo update`** (внутри цепочки) и **`helm show values`**. #### Поля YAML в POST …/addons/… (установка) Строки **`values_yaml`** / **`istio_base_values_yaml`** / **`istio_istiod_values_yaml`**: корень YAML — **object**. Пустая строка или отсутствие поля — сервер подставляет значения как при **compose-values** (кроме Istio: пустой блок = без файла **`-f`** для соответствующего чарта). Максимум **131072** символа на поле. - **ingress-nginx** — один файл **`-f`** с полным values; затем **`--set`** NodePort (сильнее файла). - **kube-prometheus-stack** — один файл: либо из **`values_yaml`**, либо автосборка из версии и полей Grafana. - **metrics-server** — аналогично. - **istio-kiali** — до трёх опциональных файлов: **istio/base**, **istio/istiod**, **kiali-server**. Если в YAML kiali-server **нет** `auth.strategy`, сервер задаёт **`auth.strategy=anonymous`** (стратегия **login** в актуальных версиях Kiali не поддерживается). Логин/пароль Kiali в теле запроса **не** передаются. **Пример POST compose-values (`kube-prometheus-stack`):** ```json { "addon": "kube-prometheus-stack", "chart_version": "65.1.1", "grafana_admin_user": "admin", "grafana_admin_password": "MySecurePwd12" } ``` **Пример ответа 200 (фрагмент):** поле **`values_yaml`** — многострочная строка с полным YAML чарта. **Пример POST установки istio-kiali с тремя YAML** (Kiali с **token**, если задано в `values_yaml`; иначе будет **anonymous**): ```json { "istio_chart_version": "1.22.0", "kiali_chart_version": "1.89.0", "istio_base_values_yaml": "defaultRevision: default\n", "istio_istiod_values_yaml": "pilot:\\n resources: {}\\n", "values_yaml": "auth:\\n strategy: token\\n" } ``` ### Фоновые задания (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?cluster=…`**, **`GET /api/v1/clusters/{name}/journal`**, страница **`GET /journal`** (режим «По кластеру»). - Операции **Helm-аддонов** (первая установка, **обновление** через тот же POST при уже установленном релизе, удаление) **дописывают** запись в **`clusters/<имя>/helm_addon_log.json`** (массив **`entries`**, новые сверху). Сводка по всем кластерам: **`GET /api/v1/journal/helm-addons`**; на странице **`/journal`** режим **Helm-аддоны** показывает тип операции и аддон в человекочитаемом виде. - Создание кластера: `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, "helm_addons_installable_count": 4, "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», а счётчик по всему снимку). - `helm_addons_installable_count` — число типовых Helm-аддонов в каталоге UI (**Аддоны**, `HELM_INSTALLABLE_ADDON_IDS` в `core/helm_addons.py`); совпадает с кнопками установки на `/cluster-addons`. - `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 --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 Записи из **`journal/jobs_history.json`**. Без параметра **`cluster`** — объединение по всем кластерам, сортировка по времени (новые первыми). С **`cluster=<имя>`** — только файл выбранного каталога (порядок как в файле, новые сверху). В каждой записи есть **`source_cluster`**. **Query:** | Параметр | Описание | |----------|----------| | `limit` | Записей на страницу, **1–100**, по умолчанию **30** | | `offset` | Смещение, **≥ 0**, по умолчанию **0** | | `cluster` | Необязательно: DNS-имя кластера — только его **`jobs_history.json`** | **Поля ответа:** **`limit`**, **`offset`**, **`total`**, **`page`**, **`total_pages`**, **`entries`** (элементы **`JournalEntryModel`** — в т.ч. **`log_lines`**). **Пример ответа 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/journal/provision Для каждого каталога **`clusters/<имя>/`**, где есть **`provision_log.json`**, читается один объект; список сортируется по **`finished_at_utc`** (новые первыми). Пагинация: **`limit`**, **`offset`** (те же ограничения, что у **`/journal/recent`**). **Ответ:** **`JournalPagedDirLogsResponse`** — в каждом элементе **`entries`** поля как у файла provision: **`kind`**, **`lines`**, **`status`**, **`message`**, **`result`**, плюс **`source_cluster`**. --- ## GET /api/v1/journal/helm-addons Аналогично **`/journal/provision`**, но источник — **`helm_addon_log.json`**: для каждого кластера разворачиваются **все** элементы массива **`entries`** (история операций Helm), строки сортируются по **`finished_at_utc`** (новые первыми). Формат одной записи совпадает с **`provision_log.json`**. --- ## 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://:<порт>` (порт из `docker port … 6443/tcp`). Если патч не удалился — отдаётся сохранённый **`kubeconfig.host`** или сырой **`kubeconfig`**. **Ошибка 404:** нет ни `kubeconfig.host`, ни `kubeconfig` в `clusters/{name}/`. **Ошибка 400:** некорректное имя кластера. --- ## GET /api/v1/clusters/{name}/kubeconfig/docker Скачать kubeconfig для **kubectl из произвольного контейнера** (`Content-Disposition`: `kubeconfig-docker-{name}.yaml`). Логика как у **kubectl внутри веб-контейнера** (`kubeconfig_patch.apply_apiserver_endpoint_to_kubeconfig_file`): при успешном **`docker port <имя>-control-plane 6443/tcp`** — **`server=https://:<порт>`** (по умолчанию **host.docker.internal**) и **`tls-server-name`** из **`KIND_K8S_KUBECONFIG_TLS_SERVER_NAME`** (по умолчанию **localhost**). Иначе запасной **`server=https://<имя>-control-plane:6443`** без SNI. На Linux у клиентского контейнера обычно нужен **`extra_hosts: host.docker.internal:host-gateway`**. **Ошибка 404:** нет файла **`kubeconfig`** в `clusters/{name}/`. **Ошибка 500:** не удалось выполнить `kubectl config set-cluster` / задать SNI. **Ошибка 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): см. раздел «Веб-интерфейс и статика» выше. --- **Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)