- Добавлен HTML SPA: internal/api/ui/iot-console.html Ванильный JS + mqtt.js (CDN), без фреймворков. Страницы: вход, список устройств, credentials, эмулятор MQTT, заглушка телеметрии. - Добавлен go:embed: internal/api/console_embed.go, GET /console - Добавлен CORS middleware в router.go для http://iot.kube5s.ru - Ingress emqx-ws-ingress.yaml: /console → sless-operator:9090 - Версия образа v0.1.53, задеплоен Доступно: http://iot.kube5s.ru/console
270 lines
14 KiB
Markdown
270 lines
14 KiB
Markdown
# Лог мышления — 2026-04-04-02
|
||
|
||
## Агент: GitHub Copilot (Claude Sonnet 4.6)
|
||
|
||
Продолжение сессии от 2026-04-04 (файл 2026-04-04.md достиг 681 строки).
|
||
|
||
---
|
||
|
||
## Задача: MQTT WebSocket через nginx-ingress — отладка 404 → 400
|
||
|
||
### Контекст
|
||
|
||
Порт 1883 заблокирован NSX-T Edge firewall на уровне облака.
|
||
Временный workaround: EMQX WebSocket listener (8083) → nginx-ingress → `iot.kube5s.ru/mqtt`.
|
||
|
||
### Что нашёл
|
||
|
||
**Проблема 1 — 404:** Ingress не получил ADDRESS (IP не присвоен).
|
||
- Причина: annotation `nginx.ingress.kubernetes.io/configuration-snippet` заблокирован в nginx-controller v1.12.6.
|
||
По умолчанию `allow-snippet-annotations=false`.
|
||
- Решение: убрал `configuration-snippet` из Ingress.
|
||
- Результат: Ingress получил sync (Scheduled for sync), но ADDRESS всё ещё пустой — это нормально для данного контроллера.
|
||
|
||
**Проблема 2 — 400:** После удаления snippet стало 400 вместо 404.
|
||
- nginx конфиг для `iot.kube5s.ru` правильный:
|
||
- `proxy_set_header Upgrade $http_upgrade` — ЕСТЬ (nginx-ingress добавляет автоматически)
|
||
- `proxy_http_version 1.1` — ЕСТЬ
|
||
- `proxy_send_timeout/read_timeout 3600s` — ЕСТЬ
|
||
- `proxy_pass → sless-emqx-ws-8083` — ЕСТЬ
|
||
- Значит 400 приходит от EMQX, а не от nginx.
|
||
|
||
### Гипотезы почему EMQX возвращает 400
|
||
|
||
EMQX WebSocket listener конфиг:
|
||
```
|
||
listeners.ws.default {
|
||
bind = "0.0.0.0:8083"
|
||
max_connections = 512
|
||
}
|
||
```
|
||
|
||
Нет явного `websocket.path`. По умолчанию EMQX ожидает путь `/mqtt`.
|
||
Но nginx из-за `pathType: Prefix` на `/mqtt` может передавать путь без trailing slash.
|
||
|
||
**Вариант A**: nginx передаёт запрос на `/mqtt` (без `/`) — EMQX ожидает `/mqtt` → должно работать.
|
||
**Вариант B**: EMQX в 5.5.1 ожидает `ws://host/mqtt` без всякого proxy path rewrite и есть баг с `X-Forwarded-Proto` — nginx шлёт `http`, а EMQX может редиректить.
|
||
**Вариант C**: `ssl_redirect: true` в nginx конфиге означает что HTTP запрос перенаправляется на HTTPS, и там 400.
|
||
|
||
### Следующий шаг
|
||
|
||
Проверить `$ssl_redirect = true` в nginx server block для нашего хоста:
|
||
- Если да — nginx делает redirect HTTP→HTTPS
|
||
- Добавить аннотацию `nginx.ingress.kubernetes.io/ssl-redirect: "false"` или тестировать через HTTPS
|
||
|
||
### Правило (новое)
|
||
|
||
Если файл `doc/thinking/ГГГГ-ММ-ДД.md` стал большим — создавать `ГГГГ-ММ-ДД-02.md`, `ГГГГ-ММ-ДД-03.md` и т.д.
|
||
|
||
---
|
||
|
||
## Задача: Анализ безопасности MQTT multitenancy
|
||
|
||
### Что исследовал
|
||
|
||
Пользователь спросил об угрозах межтенантного проникновения.
|
||
|
||
Изучил:
|
||
1. `emqx.conf` в поде — `authorization { no_match = allow }` — после CONNECT всё разрешено
|
||
2. `internal/api/handler/iot_device_handler.go` — `MQTTAuth` возвращает только `{"result":"allow"}` без ACL rules
|
||
|
||
### Вывод
|
||
|
||
**Auth (CONNECT) защищён:**
|
||
- HTTP auth endpoint проверяет namespace+deviceId+password (constant-time compare)
|
||
- enabled=true проверяется
|
||
- Secret изолирован по namespace
|
||
|
||
**ACL на pub/sub НЕТ:**
|
||
- `no_match = allow` — аутентифицированный клиент может SUBSCRIBE на любой топик
|
||
- EMQX HTTP auth plugin поддерживает возврат ACL rules в ответе на auth
|
||
- Формат ответа: `{"result":"allow","acl":[{"permission":"allow","action":"all","topic":"sless/ns/+"}]}`
|
||
- Текущий `mqttAuthResponse` содержит только `Result string` — ACL поле отсутствует
|
||
|
||
### Риски по приоритету
|
||
|
||
1. **Критично**: User A может SUBSCRIBE `sless/#` и читать все IoT данные всех пользователей
|
||
2. **Средне**: Нет rate limit на MQTT — один клиент может flood брокер
|
||
3. **Низко**: Нет TLS на 8083 (WebSocket без шифрования) — данные видны в сети
|
||
|
||
### План фикса
|
||
|
||
Добавить в `mqttAuthResponse` поле `ACL []aclRule` и возвращать из `MQTTAuth`:
|
||
```json
|
||
{
|
||
"result": "allow",
|
||
"acl": [
|
||
{"permission": "allow", "action": "publish", "topic": "sless/{ns}/{deviceId}"},
|
||
{"permission": "allow", "action": "subscribe", "topic": "sless/{ns}/{deviceId}"},
|
||
{"permission": "deny", "action": "all", "topic": "#"}
|
||
]
|
||
}
|
||
```
|
||
|
||
Ждём подтверждения от пользователя перед реализацией.
|
||
|
||
---
|
||
|
||
## Архитектурная дискуссия — IoT телеметрия и хранение данных
|
||
|
||
### Контекст разговора
|
||
|
||
Пользователь задал вопрос: "куда пишутся данные с IoT датчиков?"
|
||
|
||
Выяснилось что сейчас данные теряются — function pod получает событие но никуда не сохраняет. Это нормально для serverless (пользователь сам решает), но для IoT платформы нужно автоматическое хранение.
|
||
|
||
### Анализ сценариев использования
|
||
|
||
Реалистичные клиенты для Nubes (облачный провайдер СНГ, малый/средний бизнес):
|
||
1. Мониторинг объектов (склады, серверные, торговые точки) — температура, влажность, протечка
|
||
2. Умные счётчики / ЖКХ — снятие показаний без выезда
|
||
3. Небольшое производство / агро — теплицы, мини-заводы
|
||
|
||
Общий паттерн для всех: датчик → данные в БД → алерт если порог → график
|
||
|
||
### Решение по хранению данных
|
||
|
||
**Вопрос**: один большой Postgres или отдельный на каждого?
|
||
**Ответ**: один Postgres инстанс, но отдельная DATABASE на каждого tenant.
|
||
|
||
Причины:
|
||
- Вариант со одной таблицей + tenant_id — изоляция программная, ошибка в коде = утечка
|
||
- Отдельная DATABASE — физическая изоляция, разные connection string, разные пароли
|
||
- Клиент B не может подключиться к DATABASE клиента A даже при баге в коде платформы
|
||
|
||
Структура:
|
||
```
|
||
Postgres инстанс
|
||
├── sless_platform DB — системные таблицы (tenants, invocations)
|
||
├── tenant_abc DB — только данные клиента A
|
||
└── tenant_def DB — только данные клиента B
|
||
```
|
||
|
||
### Решение по доступу клиента
|
||
|
||
**Вопрос**: давать клиенту прямой доступ к Postgres?
|
||
**Ответ**: нет. Только через REST API платформы.
|
||
|
||
Причины:
|
||
- Postgres внутри кластера, снаружи не торчит (security)
|
||
- Единый endpoint `iot.kube5s.ru`
|
||
- Легко добавить rate limit, биллинг, кеш
|
||
- Клиент не зависит от деталей реализации хранилища
|
||
|
||
API:
|
||
```
|
||
GET /v1/namespaces/{ns}/iot/telemetry?device=X&from=T&to=T
|
||
GET /v1/namespaces/{ns}/iot/devices/{id}/last
|
||
```
|
||
|
||
### Решение по schema.sql
|
||
|
||
Клиент может положить `schema.sql` рядом с функцией. При деплое платформа выполняет его в БД tenant'а.
|
||
Это даёт низкий порог входа — клиент не шарит в Python, но может написать SQL по шаблону.
|
||
|
||
### Ключевое архитектурное решение — разделение операторов
|
||
|
||
**Решение**: sless-operator и iot-operator — ОТДЕЛЬНЫЕ компоненты.
|
||
Пока в одном кластере, но сделать так чтобы могли быть в разных.
|
||
|
||
**Namespace layout:**
|
||
```
|
||
namespace: sless — платформа sless (operator, event-dispatcher, RabbitMQ, Postgres invocations)
|
||
namespace: sless-{hash} — tenant функции (function pods)
|
||
namespace: iot — платформа IoT (iot-operator, EMQX, Postgres telemetry)
|
||
namespace: iot-{hash} — tenant IoT (IoTDevice CRDs)
|
||
```
|
||
|
||
**Связь**:
|
||
- Общий идентификатор tenant: `{hash}` одинаковый в обоих namespace
|
||
- MQTT событие → RabbitMQ в sless → function pod в sless-{hash}
|
||
- IoT operator НЕ импортирует пакеты sless-operator (loose coupling)
|
||
- Общение только через k8s API и RabbitMQ
|
||
|
||
**Postgres**:
|
||
- sless имеет свой Postgres (invocations)
|
||
- iot имеет свой Postgres (telemetry per tenant)
|
||
- Разные StatefulSet, разные PVC
|
||
|
||
### Что делает пользователь
|
||
|
||
Клиент:
|
||
1. Подключает устройство → данные автоматически пишутся в его `iot_telemetry`
|
||
2. Пишет функцию которая реагирует на события
|
||
3. Функция получает `DB_DSN` в env var (автоматически из Secret)
|
||
4. Может делать SELECT/INSERT в свою БД через обычный SQL в коде функции
|
||
5. Может читать телеметрию через REST API
|
||
|
||
### Plan — следующие шаги (этап IoT Postgres)
|
||
|
||
1. Поднять Postgres StatefulSet в namespace `iot`
|
||
2. В iot-operator при создании IoTDevice namespace → `CREATE USER`, `CREATE DATABASE`, `CREATE TABLE iot_telemetry`, `CREATE TABLE iot_devices`
|
||
3. Credentials → k8s Secret `iot-tenant-{ns}-pg`
|
||
4. В iot-mqtt-bridge при получении MQTT сообщения → INSERT в tenant БД
|
||
5. REST API endpoint для чтения телеметрии
|
||
6. При деплое function → прокинуть `DB_DSN` в env var из Secret
|
||
7. При деплое function → если есть `schema.sql` → выполнить в tenant БД
|
||
|
||
### Технические решения
|
||
|
||
- Postgres: `postgres:16-alpine` StatefulSet с PVC 10Gi в namespace `iot`
|
||
- Connection pool: pgxpool (pgx v5) per-tenant, lazy init, max 5 conn per tenant
|
||
- Таблица telemetry: `(id bigserial, device_id text, ts timestamptz default now(), payload jsonb)`
|
||
- Индекс: `(device_id, ts DESC)` для быстрых запросов по устройству за период
|
||
- Retention: пока без TTL, добавить позже через pg_partman или cron job
|
||
|
||
|
||
---
|
||
## 2026-04-04 — IoT Console UI: план и реализация
|
||
|
||
**Агент**: GitHub Copilot (Claude Sonnet 4.6)
|
||
|
||
### Постановка задачи
|
||
|
||
Пользователь сформулировал: нужен UI для управления IoT устройствами.
|
||
Причина: не все пользователи работают через Terraform/API напрямую.
|
||
Нужно: создать устройство, получить credentials, прошить в устройство, проверить отправку данных.
|
||
|
||
### Ключевое решение: эмулятор устройства в браузере
|
||
|
||
MQTT WebSocket уже работает: `ws://iot.kube5s.ru:80/mqtt`.
|
||
Браузер через mqtt.js (CDN) может подключиться как устройство напрямую.
|
||
Это значит: эмулятор — это не "симуляция", а реальная публикация MQTT сообщений.
|
||
|
||
Когда у клиента ещё нет физического устройства — он тестирует через эмулятор.
|
||
Это закрывает весь цикл без необходимости устанавливать MQTT-клиент.
|
||
|
||
### Архитектурные решения UI
|
||
|
||
**Стек**: ванильный HTML/CSS/JS + mqtt.js (CDN). Никаких фреймворков.
|
||
**Где хранить**: встраиваем в бинарник оператора через `go:embed`.
|
||
- Файл: `internal/api/ui/iot-console.html`
|
||
- Маршрут: `GET /console`
|
||
|
||
**Где доступен**: `http://iot.kube5s.ru/console`
|
||
- Ingress добавляем path `/console` → `sless-operator:9090`
|
||
|
||
**Почему не `https://sless.kube5s.ru/console`:**
|
||
- UI на HTTPS + MQTT WS без TLS = mixed content, браузер блокирует
|
||
- UI на HTTP + MQTT WS = нет mixed content, всё работает
|
||
- HTTP → HTTPS API вызовы разрешены (это не mixed content)
|
||
- Нужен только CORS на API стороне
|
||
|
||
**CORS**: заголовки `Access-Control-Allow-Origin: http://iot.kube5s.ru` + OPTIONS preflight
|
||
|
||
### Страницы
|
||
|
||
1. Вход: API адрес + MQTT брокер + namespace + токен → localStorage
|
||
2. Список устройств: таблица, создать, удалить
|
||
3. Устройство (3 вкладки):
|
||
- Credentials: username, password скрыт, топик, инструкция
|
||
- Эмулятор: подключиться → JSON payload → send / авто
|
||
- Телеметрия: "скоро"
|
||
|
||
### Следующие шаги после UI
|
||
|
||
1. Postgres StatefulSet в namespace `iot`
|
||
2. INSERT в iot_telemetry из mqtt-bridge
|
||
3. REST API для чтения телеметрии
|
||
4. Заполнить вкладку "Телеметрия" в UI
|