# Kind Clusters Dashboard — локальные кластеры Kubernetes (kind)

Проект сделан для тех, кому нужен **локальный Kubernetes без облака и без ручной возни в терминале**: **kind** остаётся движком, а поверх него — **единая панель в браузере** и **REST API**. В одном месте можно **создавать и удалять кластеры**, смотреть **узлы и поды**, ставить типовые вещи через **Helm** (ingress, мониторинг, metrics-server и др.), вести **журнал операций** и скачивать **kubeconfig** для работы с кластером с хоста или из других контейнеров. Удобно для **обучения DevOps/Kubernetes**, **быстрой проверки манифестов и Helm-чартов**, **демо** и **песочниц**, когда не хочется поднимать управляемый кластер и платить за API control plane.

Технически панель — это **FastAPI** в Docker-образе **`kind-k8s-tools:local`**, запуск через **`Makefile`** и **Compose** (**Docker** или **Podman**). На хосте по умолчанию интерфейс открывается на порту **8080** (переменная **`KIND_K8S_WEB_PORT`** в **`.env`**); **внутри контейнера** приложение слушает **6000**. Порт **6000 наружу по умолчанию не пробрасываем**: в Chrome и других браузерах на Chromium он считается небезопасным (**ERR_UNSAFE_PORT**). **kind**, **kubectl** и **Helm** ставятся в образ — на машине достаточно контейнерной среды и **make**. Данные кластеров и **kubeconfig** лежат на хосте в **`clusters/<имя>/`**.

## Документация

| Ресурс | Описание |
|--------|----------|
| **[app/docs/api_routes.md](app/docs/api_routes.md)** | Описание REST API `/api/v1/*` с примерами JSON (для фронтенда и интеграций) |
| **[app/docs/screenshots.md](app/docs/screenshots.md)** | Скриншоты веб-интерфейса (**светлая** и **тёмная** тема); файлы в **`app/docs/images/`** |
| **`/docs`** (Swagger), **`/redoc`**, **`/api/v1/health`** | На панели открываются в **отдельном окне** браузера (`window.open`); прямой URL — тот же порт, что и UI (по умолчанию **8080**) |

### Скриншоты (превью)

Полная галерея и подписи — в **[app/docs/screenshots.md](app/docs/screenshots.md)**. Ниже — главная панель в обеих темах.

| Светлая тема | Тёмная тема |
| :------------: | :-----------: |
| ![](app/docs/images/dashboard-light.png) | ![](app/docs/images/dashboard-dark.png) |

В корне репозитория — файл **`env.example`**: перечислены **только имена** переменных (без значений), для ориентира при ручной настройке **`.env`**. Полноценно создать **`.env`** можно интерактивно скриптом **`scripts/setup_env_interactive.py`** (`make setup`; в начале — выбор **docker** или **podman**, путь **`CONTAINER_SOCKET`** подставляется автоматически).

## Зачем это нужно

- Быстро получить Kubernetes **локально** (интеграционные тесты, манифесты, обучение) — см. ввод о назначении проекта выше.
- Версия кластера и число worker-нод задаются **в веб-UI** (или через REST API / скрипты в контейнере).
- Количество кластеров **не ограничено** кодом (ограничения — ресурсы хоста и Docker).
- Артефакты на хосте: `clusters/<имя>/` — том для `kubeconfig` (доступен в контейнере как `/work/clusters/<имя>/`).

## Веб-интерфейс

- Верхняя **единая карточка**: заголовок, краткое описание и строка **состояния среды** (`kind` / `kubectl` / Docker или Podman API).
- **Статистика**: число кластеров в kind, локальных каталогов, сумма workers из `meta.json`, счётчики фоновых заданий.
- **Создание кластера** (`/cluster-create`): форма с подсказкой тегов `kindest/node` (`GET /api/v1/versions`), фоновое задание и опрос статуса (JSON в сворачиваемом блоке); блок **«Последние задания»** (журнал с диска) обновляется без полной перерисовки таблицы — новые записи добавляются сверху, раскрытый лог не сбрасывается при автообновлении.
- **Кластеры** (`/clusters`): сводка ресурсов узлов (донаты), таблица кластеров — **старт**/**стоп**, скачивание kubeconfig, модалки узлов/подов, ссылка на страницу кластера; с **панели** (`/`) — кнопка **«Перейти к созданию кластера»** (размер кнопки плавно уменьшается на узком экране).
- **Аддоны** (`/cluster-addons`): выбор кластера и установка/удаление через **Helm** в контейнере — **ingress-nginx**, **kube-prometheus-stack** (логин/пароль Grafana), **metrics-server**, **Istio + Kiali** (Kiali по умолчанию без формы входа, `auth` при необходимости в YAML values); журнал операции на странице (прогресс + вывод как при создании кластера), история в **`clusters/<имя>/helm_addon_log.json`**. Нужна **пересборка образа** после обновления Dockerfile (бинарник `helm`). Таймаут операций: **`KIND_K8S_HELM_TIMEOUT_SEC`** (по умолчанию 900 с).
- **Последние задания**: история в памяти процесса (до **200** записей; после перезапуска контейнера сбрасывается); кнопка **«Очистить завершённые»** вызывает **`DELETE /api/v1/jobs`** (из памяти удаляются только завершённые задания).
- **Журнал** (`/journal`): три режима (по кластеру, развёртывание, Helm-аддоны), пагинация; на узком экране таблица записей превращается в **карточки** (как «Последние задания» на странице создания).
- **Документация** (`/documentation`): README и `app/docs/*.md` в браузере (Markdown); при загрузке и при переходе между файлами — полноэкранный **спиннер** (как на панели).
- **Автообновление** таблиц и плашки среды каждые ~3,5 с (fetch к API без перезагрузки страницы).
- При активном задании — **прогресс-бар**, **журнал** (в т.ч. скачивание образа; для **docker** при поддержке CLI — **`pull --progress=plain`**, см. **`KIND_K8S_DOCKER_PULL_PLAIN`**), опрос статуса чаще, чем общие таблицы; кнопка **«Отменить»** — прерывание с завершением текущей дочерней команды.
- Уведомления (toast) при успехе/ошибке; в подвале — копирайт и ссылка на **devops.org.ru**.

**Шапка:** навигация в виде **пилюль** (стили `.nav-pill`); при ширине окна **меньше ~920px** — кнопка **«гамбургер»** и выезжающая панель (`app/static/js/nav-mobile.js`). Пункты **Swagger**, **ReDoc** и **Health** открывают страницу в **отдельном именованном окне** (~1240×840), чтобы не уходить с панели (см. скрипт в `base.html`).

**Адаптивная вёрстка (кратко):** донаты «Ресурсы узлов» переносятся на новые строки при ширине окна **меньше ~710px**; блок **«Статистика»** на главной — мини-карточки в **две колонки** при **меньше ~520px**; таблицы **кластеров** и **«Последние задания»** на `/cluster-create` при ширине **меньше ~920px** оформляются **карточками**; на **/journal** карточки записей — при **меньше ~620px**; в журнале и таблицах заданий колонка времени (UTC) — дата и время **в две строки** при **меньше ~920px**. При первой загрузке полноэкранный спиннер: **главная**, **/clusters**, **страница кластера**, **/cluster-create**, **/documentation** (`app/static/style.css` — `.page-loading-overlay`).

**Структура фронтенда:** `app/templates/base.html` (шапка и меню), `app/templates/dashboard.html`, `app/templates/journal.html`, `app/templates/documentation.html`, `app/static/style.css`, `app/static/js/dashboard.js`, `app/static/js/journal.js`, `app/static/js/documentation.js`, `app/static/js/nav-mobile.js` (префикс API: `data-api-base` на `<body>`, по умолчанию `/api/v1`).

## Требования на хосте

| Компонент | Назначение |
|-----------|------------|
| **Docker** + **Compose v2** (или **Podman** + compose) | Сборка образа и запуск веб-сервиса |
| **make** | `make docker up` / `make podman up` и вспомогательные цели |
| **python3** | Только для **`make setup`** (создание `.env`) |

**На хост не нужны:** **kind**, **kubectl**, Python приложения — всё это в образе `kind-k8s-tools:local` и выполняется в контейнере **`kind-k8s-web`**. Проверка API и узлов: **веб-интерфейс** (кластер → узлы/поды) или **`make docker kubectl CLUSTER=<имя>`** / **`make podman kubectl …`** (см. ниже) — `kubectl` вызывается через **`docker compose exec`** / **`podman compose exec`** внутри уже запущенного сервиса.

Файл **`clusters/<имя>/kubeconfig.host`** (или скачивание из веб-UI — кнопка со стрелкой вниз) для kubectl на хосте: **`https://<KIND_K8S_KUBECONFIG_CLIENT_HOST или localhost>:<порт>`** — см. `app/kubeconfig_patch.py`. Переменная позволяет задать IP/имя хоста, если `localhost` недоступен с вашей машины. Рядом в UI — вторая кнопка (иконка «пакет»): **`GET …/kubeconfig/docker`** — kubeconfig для kubectl **из любого контейнера**: **`https://host.docker.internal:<порт>`** (порт с проброса 6443 control plane на хост) и **`tls-server-name: localhost`**, как у процесса веб-приложения. На **Linux** в вашем контейнере добавьте **`extra_hosts: ["host.docker.internal:host-gateway"]`** (в compose — как у сервиса `kind-k8s-web`). Если **`docker port`** недоступен при генерации файла, подставляется запасной **`https://<имя>-control-plane:6443`** (тогда нужна общая сеть с kind).

Смонтированы **сокет** Docker/Podman и каталог **`./clusters`** → в контейнере **`/work/clusters`**. Каталог **`./app`** монтируется в **`/opt/kind-k8s/app`** для разработки без пересборки образа. Файл **`./README.md`** монтируется в **`/opt/kind-k8s/README.md`** (страница **«Документация»** и **`GET /api/v1/docs/readme`** без пересборки образа).

После создания кластера при **`KIND_K8S_PATCH_KUBECONFIG`** дополнительно пишется **`kubeconfig.host`**; скачивание через API каждый раз пересобирает файл с актуальным портом и хостом из **`KIND_K8S_KUBECONFIG_CLIENT_HOST`**.

### kubectl из другого контейнера (ошибка `localhost:<порт>` / `dev-control-plane: Name or service not known`)

Файл **`kubeconfig.host`** рассчитан на запуск **kubectl с хоста**. Внутри **другого** контейнера **`127.0.0.1` — не хост Docker**, поэтому **`https://localhost:<порт>`** из `kubeconfig.host` там не работает. Имя **`…-control-plane`** резолвится только в **той же сети**, что и узел kind — из «чужой» сети контейнера DNS его не видит.

**Вариант 1 (предпочтительно): скачать kubeconfig из UI** (кнопка с иконкой пакета) или **`GET /api/v1/clusters/{name}/kubeconfig/docker`**. В файле уже **`https://host.docker.internal:<порт>`** и **`tls-server-name: localhost`** (как у kubectl внутри `kind-k8s-web`): трафик идёт на **хост**, затем в проброшенный порт API kind — **без** общей сети с кластером. В **вашем** контейнере на **Linux** добавьте в compose/run: **`extra_hosts: ["host.docker.internal:host-gateway"]`** (как в `docker-compose.yml` у `kind-k8s-web`). Шлюз переопределяется **`KIND_K8S_APISERVER_GATEWAY_HOST`**; SNI — **`KIND_K8S_KUBECONFIG_TLS_SERVER_NAME`**.

**Вариант 2: общая сеть Docker с kind.** Подключите контейнер к сети kind и задайте **`server: https://<имя_кластера>-control-plane:6443`** (см. `docker network ls` / `docker inspect …-control-plane`).

Подробности — **`app/kubeconfig_patch.py`**.

## Быстрый старт

### Готовый образ с Docker Hub (без сборки, без файла `.env`)

**Опубликованный образ:** **`inecs/kind-cluster-dashboard:v1.0.0`** (соответствует значениям по умолчанию в `Makefile`: `DOCKERHUB_REPO=inecs/kind-cluster-dashboard`, `RELEASE_TAG=v1.0.0`). Все теги: [hub.docker.com/r/inecs/kind-cluster-dashboard/tags](https://hub.docker.com/r/inecs/kind-cluster-dashboard/tags).

Сборка и отправка в Docker Hub (для сопровождения образа): **linux/amd64** (ПК **x86_64**) и **linux/arm64** (**Apple Silicon** и прочий ARM). Образ **linux/386** (32-bit x86) для **kind** официальными бинарниками не поставляется, поэтому в релиз не входит.

```bash
docker login
# По умолчанию пушит inecs/kind-cluster-dashboard:v1.0.0 (см. Makefile):
make release
# Или явно: make release DOCKERHUB_REPO=inecs/kind-cluster-dashboard RELEASE_TAG=v1.0.0
# Необязательно: KIND_VERSION=0.24.0 HELM_VERSION=v3.16.3 KUBECTL_VERSION=v1.32.0
```

Другой образ/тег задайте в shell: **`KIND_K8S_HUB_IMAGE=логин/репозиторий:тег`** при вызове compose (в YAML ниже то же через подстановку).

```bash
mkdir -p clusters
# Docker (файл в репозитории: compose/docker-compose.hub.docker.yml; образ по умолчанию — v1.0.0):
docker compose -f compose/docker-compose.hub.docker.yml up -d

# Другой тег: KIND_K8S_HUB_IMAGE=inecs/kind-cluster-dashboard:latest docker compose -f compose/docker-compose.hub.docker.yml up -d

# Podman — путь к API-сокету только через переменные окружения (не через .env):
export CONTAINER_SOCKET="${XDG_RUNTIME_DIR}/podman/podman.sock"
podman compose -f compose/docker-compose.hub.podman.yml up -d
```

Браузер: **http://127.0.0.1:8080** (в примерах ниже публикация **8080:6000**; при необходимости поменяйте в YAML).

Ниже — **полные** примеры `docker-compose`: все переменные **`environment`** заданы явно теми же значениями, что использует приложение по умолчанию (см. `docker-compose.yml`, `app/core/*.py`, `app/kubeconfig_patch.py`). **`.env` не нужен**. Актуальные копии — в [`compose/docker-compose.hub.docker.yml`](compose/docker-compose.hub.docker.yml) и [`compose/docker-compose.hub.podman.yml`](compose/docker-compose.hub.podman.yml).

**Docker** — вставьте в `docker-compose.yml` (или сохраните как отдельный файл):

```yaml
services:
  kind-k8s-web:
    image: ${KIND_K8S_HUB_IMAGE:-inecs/kind-cluster-dashboard:v1.0.0}
    container_name: kind-clusters-dashboard
    user: "0:0"
    volumes:
      - ${KIND_K8S_CLUSTERS_DIR:-./clusters}:/work/clusters
      - ${CONTAINER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
    ports:
      - "8080:6000"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      HOME: /root
      DOCKER_HOST: unix:///var/run/docker.sock
      KIND_K8S_IN_CONTAINER: "1"
      KIND_K8S_WORKDIR: /work
      KIND_K8S_PATCH_KUBECONFIG: "1"
      KIND_K8S_KUBECONFIG_CLIENT_HOST: localhost
      KIND_K8S_KUBECONFIG_TLS_SERVER_NAME: localhost
      KIND_K8S_APISERVER_GATEWAY_HOST: host.docker.internal
      CONTAINER_CLI: docker
      KIND_K8S_SKIP_VERSION_LIST: "0"
      KIND_K8S_VERSION_LIST_DISPLAY: "50"
      KIND_K8S_HUB_TAGS_MAX_PAGES: "120"
      KIND_K8S_DEBUG: "0"
      KIND_K8S_JOB_LOG_MAX_LINES: "2500"
      KIND_K8S_STREAM_PTY: "1"
      KIND_K8S_DOCKER_PULL_PLAIN: "1"
      KIND_K8S_JOB_API_LOG_MAX_LINES: "5000"
      KIND_K8S_JOBS_JSON: /work/clusters/kind_k8s_jobs.json
      KIND_K8S_README_PATH: /opt/kind-k8s/README.md
      KIND_K8S_WAIT_NODES: "1"
      KIND_K8S_WAIT_NODES_TIMEOUT_SEC: "300"
      KIND_K8S_HELM_TIMEOUT_SEC: "900"
      KIND_K8S_HELM_VERSIONS_CACHE_SEC: "600"
      KIND_K8S_HELM_VERSIONS_MAX: "80"
      KIND_K8S_CLUSTER_JOURNAL_MAX_ENTRIES: "500"
      KIND_K8S_CLUSTER_JOURNAL_MAX_LOG_LINES: "2000"
      KIND_K8S_HELM_ADDON_LOG_MAX_ENTRIES: "500"
      KIND_K8S_APP_TITLE: "Kind Clusters Dashboard"
      KIND_K8S_UVICORN_RELOAD: "0"
    working_dir: /opt/kind-k8s/app
    command: ["/opt/kind-k8s/run_uvicorn.sh"]
```

**Podman** — перед запуском: **`export CONTAINER_SOCKET="$XDG_RUNTIME_DIR/podman/podman.sock"`** (или фактический путь к `podman.sock`). При **EACCES** на сокет подберите **`user`** / **UID:GID** как в основном [`docker-compose.yml`](docker-compose.yml) и [`Makefile`](Makefile) для `make podman up`.

```yaml
services:
  kind-k8s-web:
    image: ${KIND_K8S_HUB_IMAGE:-inecs/kind-cluster-dashboard:v1.0.0}
    container_name: kind-clusters-dashboard
    userns_mode: keep-id
    user: "0:0"
    volumes:
      - ${KIND_K8S_CLUSTERS_DIR:-./clusters}:/work/clusters
      - ${CONTAINER_SOCKET}:/run/podman/podman.sock
    ports:
      - "8080:6000"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      HOME: /tmp
      DOCKER_HOST: unix:///run/podman/podman.sock
      KIND_K8S_IN_CONTAINER: "1"
      KIND_K8S_WORKDIR: /work
      KIND_K8S_PATCH_KUBECONFIG: "1"
      KIND_K8S_KUBECONFIG_CLIENT_HOST: localhost
      KIND_K8S_KUBECONFIG_TLS_SERVER_NAME: localhost
      KIND_K8S_APISERVER_GATEWAY_HOST: host.docker.internal
      CONTAINER_CLI: podman
      KIND_K8S_SKIP_VERSION_LIST: "0"
      KIND_K8S_VERSION_LIST_DISPLAY: "50"
      KIND_K8S_HUB_TAGS_MAX_PAGES: "120"
      KIND_K8S_DEBUG: "0"
      KIND_K8S_JOB_LOG_MAX_LINES: "2500"
      KIND_K8S_STREAM_PTY: "1"
      KIND_K8S_DOCKER_PULL_PLAIN: "1"
      KIND_K8S_JOB_API_LOG_MAX_LINES: "5000"
      KIND_K8S_JOBS_JSON: /work/clusters/kind_k8s_jobs.json
      KIND_K8S_README_PATH: /opt/kind-k8s/README.md
      KIND_K8S_WAIT_NODES: "1"
      KIND_K8S_WAIT_NODES_TIMEOUT_SEC: "300"
      KIND_K8S_HELM_TIMEOUT_SEC: "900"
      KIND_K8S_HELM_VERSIONS_CACHE_SEC: "600"
      KIND_K8S_HELM_VERSIONS_MAX: "80"
      KIND_K8S_CLUSTER_JOURNAL_MAX_ENTRIES: "500"
      KIND_K8S_CLUSTER_JOURNAL_MAX_LOG_LINES: "2000"
      KIND_K8S_HELM_ADDON_LOG_MAX_ENTRIES: "500"
      KIND_K8S_APP_TITLE: "Kind Clusters Dashboard"
      KIND_K8S_UVICORN_RELOAD: "0"
    working_dir: /opt/kind-k8s/app
    command: ["/opt/kind-k8s/run_uvicorn.sh"]
```

### Запуск из репозитория (локальная сборка образа)

```bash
# Из корня клонированного репозитория Kind Clusters Dashboard (рядом с Makefile):
make setup           # опционально: интерактивно создать .env (Enter — дефолты из скрипта)
make docker check-docker    # или: make podman check-docker
make docker up       # или: make podman up
# Браузер: http://127.0.0.1:8080  (порт: KIND_K8S_WEB_PORT в .env; не 6000 на хосте — Chrome ERR_UNSAFE_PORT)
```

Из родительского каталога: `make -C <каталог-корня-репозитория> docker up` (подставьте путь к каталогу с `Makefile`).

**Логи, статус, перезапуск и остановка:** `make docker logs` / `make podman logs` (follow), `make docker ps` / `make podman ps`, **`make docker restart` / `make podman restart`** (перезапуск сервиса `kind-k8s-web`), `make docker down` / `make podman down`.

### Разработка UI и API без пересборки образа

В **`docker-compose.yml`** смонтированы **`./app`** → **`/opt/kind-k8s/app`** и **`./README.md`** → **`/opt/kind-k8s/README.md`** (только чтение).

По умолчанию (**`KIND_K8S_UVICORN_RELOAD=1`**) uvicorn запускается с **`--reload`** (см. **`scripts/run_uvicorn.sh`**) и перезапускает процесс при изменении `*.py`, `*.html`, `*.css`, `*.js` в `app/`. Пересобирать образ нужно после изменений **Dockerfile**, **`requirements.txt`** или **`scripts/run_uvicorn.sh`**.

Отключить reload: в **`.env`** задать **`KIND_K8S_UVICORN_RELOAD=0`**.

### Дополнительно: CLI в одноразовом контейнере

Если нужен сценарий без UI (CI, скрипты):

```bash
docker compose run --rm --entrypoint python3 kind-k8s-web \
  /opt/kind-k8s/app/create_cluster.py --non-interactive --name dev --kubernetes-version 1.29.4 --workers 2

docker compose run --rm --entrypoint python3 kind-k8s-web \
  /opt/kind-k8s/app/delete_cluster.py --non-interactive --name dev --yes
```

Рабочий каталог сервиса в образе — `/opt/kind-k8s/app`; том `clusters/` и сокет те же, что у `docker compose up`.

### kubectl без установки на хост

Пока запущен веб-сервис (`make docker up` или `make podman up`), **kubectl** из образа:

```bash
# По умолчанию: get nodes (kubeconfig: /work/clusters/<имя>/kubeconfig внутри контейнера)
make docker kubectl CLUSTER=<имя_кластера>
# или: make podman kubectl CLUSTER=<имя_кластера>

make docker kubectl CLUSTER=<имя> KUBECTL_ARGS="get pods -A"
make docker kubectl CLUSTER=<имя> KUBECTL_ARGS="config view --minify"
```

Эквивалент вручную (из корня репозитория, **Docker**):

```bash
docker compose exec kind-k8s-web kubectl --kubeconfig=/work/clusters/<имя>/kubeconfig get nodes
```

После успешного `kind create` по умолчанию выполняется **`kubectl wait`** готовности нод (`KIND_K8S_WAIT_NODES`, `KIND_K8S_WAIT_NODES_TIMEOUT_SEC` в `.env`) — тоже **внутри** контейнера приложения.

## Команды Makefile

| Цель | Описание |
|------|----------|
| `make help` | Краткая справка |
| `make docker up` / `make podman up` | Поднять веб-UI (`kind-k8s-web`) |
| `make docker down` / `make podman down` | Остановить compose в каталоге репозитория |
| `make docker restart` / `make podman restart` | Перезапустить контейнер сервиса `kind-k8s-web` (`compose restart`) |
| `make docker logs` / `make podman logs` | Логи `kind-k8s-web` (stream, `-f`) |
| `make docker ps` / `make podman ps` | Статус контейнеров текущего compose-проекта |
| `make docker build` / `make podman build` | Собрать образ `kind-k8s-tools:local` |
| `make docker rebuild` / `make podman rebuild` | Пересборка образа **без кэша** (`build --no-cache`) и пересоздание контейнера (`up -d --force-recreate`) |
| `make docker check-docker` / `make podman check-docker` | Проверить выбранный CLI и `compose version` |
| `make docker kubectl CLUSTER=…` / `make podman kubectl CLUSTER=…` | **kubectl** в контейнере `kind-k8s-web` (опционально `KUBECTL_ARGS="…"`; по умолчанию `get nodes`). Сервис должен быть **up**. |
| `make setup` | Интерактивно создать `.env` (список переменных в `scripts/setup_env_interactive.py`) |
| `make clusters-dir` | Создать каталог `clusters/` |
| `make docker …` / `make podman …` | Префикс **обязателен** для целей `up`, `down`, `restart`, `logs`, `ps`, `build`, `rebuild`, `check-docker`, `kubectl` |

Цели `up`, `down`, `restart`, `logs`, `ps`, `build`, `rebuild`, `check-docker` и `kubectl` **без** `docker`/`podman` в той же команде завершатся с подсказкой.

## Переменные окружения

Файл **`.env`** в корне репозитория подхватывает Compose. Создать его: **`make setup`** или вручную по списку в **`scripts/setup_env_interactive.py`**. Файл **`.env`** в git не коммитится (см. `.gitignore`).

Переменные **`DOCKER_HOST`**, **`KIND_K8S_IN_CONTAINER`**, **`KIND_K8S_WORKDIR`** в контейнере задаются **литералами в `docker-compose.yml`**, а не из `.env`.

| Переменная | Где используется | Назначение |
|------------|------------------|------------|
| **`KIND_VERSION`** | build-arg | Версия бинарника kind при сборке образа |
| **`KUBECTL_VERSION`** | build-arg | Версия kubectl в образе; пусто в compose → в Dockerfile подставляется `stable.txt` при сборке; `make setup` предлагает закреплённый тег |
| **`KIND_K8S_WEB_PORT`** | ports | Порт **на хосте** для веб-UI (по умолчанию **8080**; в контейнере публикация идёт на процесс на **6000**) |
| **`KIND_K8S_WEB_HOST`** | локальный uvicorn / Settings | Хост привязки при запуске вне compose (в контейнере задаётся entrypoint) |
| **`KIND_K8S_UVICORN_RELOAD`** | контейнер | `1` (по умолчанию) — hot-reload при правках в `./app`; `0` — без reload |
| **`KIND_K8S_APP_TITLE`** | контейнер / Settings | Заголовок OpenAPI и HTML; пустое значение из compose не ломает приложение (`env_ignore_empty`, fallback) |
| **`KIND_K8S_WAIT_NODES`** | контейнер | `0` — не ждать Ready нод после create |
| **`KIND_K8S_WAIT_NODES_TIMEOUT_SEC`** | контейнер | Таймаут `kubectl wait` (секунды) |
| **`CONTAINER_SOCKET`** | volume | Путь к сокету **на хосте**; при **`make podman …`** перед compose пишется **`.env.podman.override`** (в **`.gitignore`**) с актуальным путём из **`scripts/detect_podman_socket.py`**, чтобы перекрыть **`.env`**: иначе старый **`docker-compose`** часто подставляет том из **`.env`** (в т.ч. неверный сокет **`/var/folders/…/podman-machine-default-api.sock`**) и падает с **operation not supported** |
| **`CONTAINER_SOCKET_MOUNT_TARGET`** | volume | Путь **внутри** контейнера: Docker — `/var/run/docker.sock`; Podman — **`/run/podman/podman.sock`** |
| **`KIND_K8S_REMOTE_SOCKET_URI`** | контейнер `DOCKER_HOST` | URI API (совпадает с точкой монтирования), например **`unix:///run/podman/podman.sock`** для Podman |
| **`KIND_K8S_PATCH_KUBECONFIG`** | контейнер | Патч `server` в kubeconfig для хоста; по умолчанию **включено** (`1` в compose и в `make setup`) |
| **`CONTAINER_CLI`** | контейнер | CLI для `docker port` / `podman port` (`docker` или `podman`) |
| **`KIND_K8S_CONTAINER_UIDGID`** | compose `user` | **`uid:gid`** процесса в контейнере; для Docker обычно **`0:0`**; для rootless Podman — **`$(id -u):$(id -g)`** (пишет **`make setup`**) |
| **`KIND_K8S_CONTAINER_HOME`** | контейнер `HOME` | Для не-root в образе без `/home/<uid>`: **`/tmp`** (Podman); для root — **`/root`** |
| **`KIND_K8S_SKIP_VERSION_LIST`** | контейнер | Не ходить в Docker Hub за тегами |
| **`KIND_K8S_VERSION_LIST_DISPLAY`** | контейнер | Сколько строк показывать в **интерактивном CLI** при выборе версии (веб-UI выводит полный список из API) |
| **`KIND_K8S_HUB_TAGS_MAX_PAGES`** | контейнер | Сколько страниц Docker Hub обходить при сборе тегов (старые 1.19.x часто на поздних страницах; в коде по умолчанию **120**, максимум **500**) |
| **`KIND_K8S_DEBUG`** | контейнер | `1`/`true`/`yes`/`да` — уровень DEBUG в логах |
| **`KIND_K8S_JOB_LOG_MAX_LINES`** | приложение | Сколько строк журнала хранить в памяти на задание (старые вытесняются); по умолчанию **2500** |
| **`KIND_K8S_JOB_API_LOG_MAX_LINES`** | приложение | Сколько строк отдавать в **GET /api/v1/jobs/{id}** (хвост); по умолчанию **5000**, максимум **20000** |
| **`KIND_K8S_JOBS_JSON`** | приложение | Путь к JSON с историей заданий; пусто — **`clusters/kind_k8s_jobs.json`** под **`KIND_K8S_WORKDIR`** |
| **`KIND_K8S_STREAM_PTY`** | приложение | **`1`** (по умолчанию) — для `kind` и **podman pull** псевдо-TTY; **`0`** — только pipe |
| **`KIND_K8S_DOCKER_PULL_PLAIN`** | приложение | **`1`** (по умолчанию) — если в выводе **`docker pull --help`** есть **`--progress`**, используется **`docker pull --progress=plain`** без PTY; иначе обычный pull. **`0`** — никогда не добавлять флаг |
| **`KIND_K8S_README_PATH`** | контейнер / приложение | Абсолютный путь к **README.md** для страницы **`/documentation`**; если пусто — используется `README.md` рядом с каталогом `app/` (в образе: `/opt/kind-k8s/README.md`) |
| **`KIND_K8S_WORKDIR`** | локальный запуск | Корень данных на машине разработчика без compose |
| **`COMPOSE_BUILD_FLAGS`** | Makefile | Например `make docker build COMPOSE_BUILD_FLAGS=--platform linux/arm64` (то же для **`make docker rebuild`**) |

## Podman (пример rootless)

```bash
export CONTAINER_SOCKET="$XDG_RUNTIME_DIR/podman/podman.sock"
make podman up
```

**Доступ к сокету (`permission denied` на `/var/run/docker.sock` внутри контейнера):** у rootless Podman сокет обычно принадлежит вашему пользователю, а не «root» из контейнера. В **`.env`** для Podman нужны **`KIND_K8S_CONTAINER_UIDGID=$(id -u):$(id -g)`** и **`KIND_K8S_CONTAINER_HOME=/tmp`** — при **`make setup`** с выбором **Podman** скрипт записывает их сам. Команды **`make podman …`** подмешивают **`docker-compose.podman.yml`** (`userns_mode: keep-id`) и дублируют uid/gid в окружении: без **keep-id** rootless Podman часто даёт **permission denied** на смонтированный сокет даже при верном `user:`. Вручную: **`podman compose -f docker-compose.yml -f docker-compose.podman.yml up -d`** и те же переменные в **`.env`** или в shell. На системах с SELinux при отказе доступа к сокету попробуйте **`CONTAINER_SOCKET_VOLUME_OPTS=:Z`** в `.env`.

## Структура репозитория (основное)

| Путь | Назначение |
|------|------------|
| `Makefile` | Запуск веб-UI; префикс `docker` или `podman` обязателен; цели `up`, `down`, `restart`, `logs`, `rebuild`, `build` и др. Для Podman: **`--env-file .env.podman.override`** (нужен compose с поддержкой **`--env-file`**, обычно ≥ 1.28). |
| `.env.podman.override` | Создаётся целью **`make podman …`**: `CONTAINER_SOCKET`, `KIND_K8S_*` для сокета; перекрывает устаревшие значения в **`.env`**. |
| `scripts/setup_env_interactive.py` | Интерактивное создание `.env` (все ключи и дефолты внутри скрипта) |
| `scripts/run_uvicorn.sh` | Точка входа контейнера: uvicorn с опциональным `--reload` |
| `Dockerfile` | Образ: kind, kubectl, docker-cli, FastAPI |
| `requirements.txt` | pip-зависимости веб-приложения |
| `docker-compose.yml` | Сервис `kind-k8s-web`, тома `./clusters`, `./app`, `./README.md`, сокет |
| `docker-compose.podman.yml` | Только для **Podman**: `userns_mode: keep-id` (подмешивается в **make podman …**) |
| `app/main.py` | FastAPI: главная `/`, создание кластера `/cluster-create`, `/documentation`, редирект `/ui`, монтирование `/static` |
| `app/api/v1/` | REST API: `router.py`, `endpoints/` (`health`, `versions`, `docs_readme`, `clusters`) |
| `app/core/` | Жизненный цикл кластеров, задания, настройки, блокировки (`kind_guard`), пути |
| `app/models/schemas.py` | Pydantic-схемы запросов/ответов API |
| `app/templates/` | Jinja2: `base.html`, `dashboard.html`, `clusters.html`, `cluster_create.html`, `cluster_detail.html`, `cluster_edit.html`, `cluster_addons.html`, `journal.html`, `documentation.html` |
| `app/static/` | `style.css`, `js/dashboard.js`, `js/documentation.js`, `js/vendor/` (marked, DOMPurify для README в UI) |
| `app/docs/` | `api_routes.md` (описание REST API) |
| `app/create_cluster.py`, `delete_cluster.py`, `cluster_status.py` | CLI и переиспользование из API / `compose run` |

В UI и API список версий **kindest/node** по умолчанию тянется с Docker Hub (нужна сеть). В изолированной среде: **`KIND_K8S_SKIP_VERSION_LIST=1`** — версию вводят вручную.

## Где лежат данные на хосте

- `clusters/<имя>/kind-config.yaml`
- `clusters/<имя>/kubeconfig`
- `clusters/<имя>/meta.json`

Содержимое `clusters/*/` не коммитится; в репозитории есть `clusters/.gitkeep`.

## Git и артефакты

В **`.gitignore`**: `.env`, каталоги в `clusters/` (кроме `.gitkeep`), **`__pycache__/`** и `*.pyc`, `.DS_Store`.

## Ограничения

- Образ `kindest/node:v…` должен быть доступен для pull.
- На **Windows** без WSL удобнее WSL2 + Docker Desktop.
- **kubectl** на хосте **не обязателен**: используйте веб-UI или **`make docker kubectl`** / **`make podman kubectl`** (см. выше).
- История заданий в UI/API хранится в памяти (до **200** записей); после перезапуска контейнера очищается. Завершённые записи можно удалить из памяти кнопкой на панели или **`DELETE /api/v1/jobs`**.
- При **`exec format error`** у kind пересоберите образ: `make docker rebuild COMPOSE_BUILD_FLAGS=--platform linux/arm64` (или `make podman …`, или **`make docker build`** без `--no-cache`, или `linux/amd64`).

---

**Автор:** Сергей Антропов — [devops.org.ru](https://devops.org.ru)
