Files
KindClustersDashboard/app/docs/api_routes.md
Sergey Antropoff eb063aec20 Веб-интерфейс: страница /clusters, навигация и крошки для кластеров
- Выделена страница списка кластеров, панель упрощена; nav_active и крошки
  ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL.
- Доработки дашборда, аддонов, журнала, стилей и API-документации.
- Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
2026-04-04 13:42:21 +03:00

54 KiB
Raw Blame History

Описание REST API веб-интерфейса Kind Clusters Dashboard

Базовый префикс: /api/v1
Автор: Сергей Антропов — 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 Панель: CTA создания кластера, карточка Статистика (среда kind/kubectl, счётчики), отдельная карточка Ресурсы узлов (сводка) (донаты по GET /api/v1/stats); полная таблица кластеров — на GET /clusters.
GET /clusters HTML Кластеры: шапка с кнопкой Создать кластер (/cluster-create), сводка Ресурсы узлов, таблица кластеров (старт/стоп, ссылка на GET /cluster/<имя>, модалки как на панели); скрипт dashboard.js.
GET /cluster/{name} HTML страница кластера: донаты «Ресурсы узлов (сводка)», карточки узлов, таблицы 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, POST /helm/addons/compose-values (подстановка реальных helm show values + логины в редактор), три YAML для Istio, установка/удаление релизов.
GET /journal HTML Журналы: переключатель (как «Простой/Расширенный» в редактировании кластера) — по кластеру (journal/recent?cluster=), развёртывание (/journal/provision), Helm-аддоны (/journal/helm-addons); пагинация по 30 записей.
GET /documentation HTML-оболочка; documentation.js: без pathGET /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/clusters.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; тело: опционально 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, result; пароли в файл не пишутся.

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; секрет логина Kiali и auth.strategy=login отдельно.

Пример POST compose-values (kube-prometheus-stack):

{
  "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_username": "kiali-admin",
  "kiali_password": "MySecurePwd12",
  "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-аддонов (install/delete) дописывают запись в clusters/<имя>/helm_addon_log.json (массив entries, новые сверху), тот же набор полей, что у provision_log.json. Сводка по всем кластерам: GET /api/v1/journal/helm-addons.
  • Создание кластера: POST /api/v1/clusters → опрос GET /api/v1/jobs/{job_id} (как в веб-UI).
  • В ответе задания поля progress_stage (текст этапа) и progress_percent (0100) обновляются во время создания.
  • В GET /api/v1/jobs (список) поле progress_log всегда пустой массив — меньше трафика; полный хвост — в GET /api/v1/jobs/{job_id} (лимит строк: KIND_K8S_JOB_API_LOG_MAX_LINES, по умолчанию 5000).
  • Буфер строк в памяти на задание: KIND_K8S_JOB_LOG_MAX_LINES (по умолчанию 2500); при переполнении старые строки вытесняются. Для 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):

{
  "status": "ok",
  "kind_in_path": true,
  "kubectl_in_path": true,
  "container_cli": "docker",
  "container_engine_ok": true,
  "container_engine_detail": null
}

Если сокет Docker недоступен:

{
  "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.

Пример запроса:

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):

# Описание 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:

{
  "tags": ["latest", "v1.32.0", "v1.31.4"],
  "skipped": false
}

Пример при пропуске загрузки:

{
  "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:

{
  "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 (шкала 0100 относительно порога 8 GiB на полное кольцо).

GET /api/v1/clusters

Список: объединение kind get clusters и подкаталогов clusters/* (без дубликатов).

Пример ответа 200 (массив):

[
  {
    "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:

{
  "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"]
}

При недоступном helm503 с текстом ошибки.


GET /api/v1/journal/recent

Записи из journal/jobs_history.json. Без параметра cluster — объединение по всем кластерам, сортировка по времени (новые первыми). С cluster=<имя> — только файл выбранного каталога (порядок как в файле, новые сверху). В каждой записи есть source_cluster.

Query:

Параметр Описание
limit Записей на страницу, 1100, по умолчанию 30
offset Смещение, ≥ 0, по умолчанию 0
cluster Необязательно: DNS-имя кластера — только его jobs_history.json

Поля ответа: limit, offset, total, page, total_pages, entries (элементы JournalEntryModel — в т.ч. log_lines).

Пример ответа 200 (первая страница, до 30 записей):

{
  "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:

{
  "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 (1200, по умолчанию 30).

Пример ответа 200 (массив JobView):

[
  {
    "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:

{
  "removed": 4
}

POST /api/v1/jobs/{job_id}/cancel

Прервать фоновое задание (create_cluster, start_cluster, start_cluster_reapply, start_containers, stop_containers). Для длительной команды завершается связанный дочерний процесс; между шагами запуска/остановки отдельных узлов также проверяется флаг отмены.

Пример ответа 200:

{
  "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):

{
  "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-нод 020 (вместе с версией из запроса или из 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 не используются (можно не отправлять).

Пример тела (только описание):

{
  "description": "Обновлённая заметка"
}

Пример тела (простой режим):

{
  "kubernetes_version": "1.30.0",
  "workers": 3,
  "description": "Три воркера"
}

Пример тела (расширенный YAML):

{
  "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):

{
  "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):

{
  "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:

{
  "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 (фрагмент):

{
  "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):

{
  "namespace": "default",
  "pod": "my-app-7d4f8b9-xk2cp"
}

Пример ответа 200:

{
  "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:

{
  "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):

{
  "name": "dev",
  "kubernetes_version": "1.29.4",
  "workers": 2
}

Пример ответа 202:

{
  "job_id": "a1b2c3d4e5f6...",
  "status": "queued",
  "message": "Создание кластера выполняется в фоне; опросите GET /api/v1/jobs/{job_id}"
}

Ошибка 400: невалидное имя кластера или тело не проходит валидацию Pydantic.

Ошибка 409 (кластер уже есть в kind):

{
  "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 (запуск узлов):

{
  "job_id": "cafebabe...",
  "status": "queued",
  "mode": "containers",
  "message": "Запуск узлов; опросите GET /api/v1/jobs/{job_id}"
}

Пример ответа 202 (подъём по конфигу):

{
  "job_id": "deadbeef...",
  "status": "queued",
  "mode": "kind_config",
  "message": "Подъём кластера по сохранённому конфигу; опросите GET /api/v1/jobs/{job_id}"
}

Пример ответа 202 (пересоздание после правки YAML):

{
  "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:

{
  "job_id": "baba...",
  "status": "queued",
  "mode": "stop",
  "message": "Остановка узлов; опросите GET /api/v1/jobs/{job_id}"
}

Ошибка 400: некорректное имя кластера.


GET /api/v1/jobs/{job_id}

Статус фонового задания создания.

В процессе (пример 200):

{
  "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):

{
  "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:

{
  "detail": "Задание не найдено"
}

DELETE /api/v1/clusters/{name}

kind delete cluster и удаление каталога clusters/{name}/.

Пример ответа 200:

{
  "name": "dev",
  "kind_delete_ok": true,
  "summary": "kind delete: OK; удалена папка /work/clusters/dev"
}

Ошибка 400: некорректное имя кластера.
Ошибка 500: логическая ошибка удаления (тело с detail).


GET /

HTML-дашборд (не JSON): см. раздел «Веб-интерфейс и статика» выше.