- Разбор проблемы crypto.subtle (HTTP → HTTPS) - Проблема 404 после apply (Docker layer cache) - Убраны поля API/MQTT из формы входа - Ребрендинг Nubes: палитра #001C34, логотип SVG, favicon - Таблица версий v0.1.54–v0.1.58 с коммитами
391 lines
21 KiB
Markdown
391 lines
21 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
|
||
|
||
---
|
||
# Агент: GitHub Copilot (Claude Sonnet 4.6) — продолжение сессии 2026-04-04
|
||
|
||
## Исправления и улучшения IoT Console UI (v0.1.53 → v0.1.58)
|
||
|
||
### Проблема 1: `crypto.subtle.digest` — Cannot read properties of undefined
|
||
|
||
**Симптом:** Пользователь вставил токен, получил ошибку "Cannot read properties of undefined (reading 'digest')".
|
||
|
||
**Анализ:** `crypto.subtle` доступен ТОЛЬКО на HTTPS-страницах (Secure Context). Консоль раздавалась по HTTP (`http://iot.kube5s.ru/console`). На HTTP `crypto.subtle === undefined`.
|
||
|
||
**Решение:** Перевести консоль на HTTPS — это устранит корень проблемы и заодно уберёт необходимость в pure-JS SHA256. Попытка написать pure-JS SHA256 была правильной как fallback, но правильнее — исправить инфраструктуру.
|
||
|
||
**Действия:**
|
||
1. `emqx-ws-ingress.yaml`: добавлена TLS-секция + `cert-manager.io/cluster-issuer: letsencrypt-prod`, `ssl-redirect: "true"`, `secretName: iot-kube5s-ru-tls`
|
||
2. `router.go`: CORS `Allow-Origin`: `http://` → `https://iot.kube5s.ru`
|
||
3. `iot-console.html`: дефолт MQTT брокера `ws://` → `wss://`
|
||
4. cert-manager автоматически выпустил сертификат Let's Encrypt (READY: True за ~34 сек)
|
||
5. Собрали v0.1.54, задеплоили
|
||
|
||
**Косяк при apply:** `kubectl apply` взял старый Ingress из кэша (только путь `/mqtt`, без `/console`). Пришлось использовать `kubectl replace` вместо `apply`.
|
||
|
||
**Итог:** `https://iot.kube5s.ru/console` → 200, TLS v1.3, `CN=iot.kube5s.ru`, Let's Encrypt R13. `crypto.subtle` заработал.
|
||
|
||
---
|
||
|
||
### Проблема 2: 404 после перехода на HTTPS (v0.1.54)
|
||
|
||
**Симптом:** После `kubectl apply` + rollout — curl возвращал 404.
|
||
|
||
**Анализ:** Запрос доходил до пода (видно в логах), но оператор отвечал 404. Значит маршрут `/console` не регистрировался. Проверили: файл `iot-console.html` существует на диске, `go:embed` прописан, маршрут в `router.go` есть. **Причина:** первый `docker build` взял Go-слои из кэша Docker — старый бинарь без `/console` маршрута.
|
||
|
||
**Решение:** Пересборка с `--no-cache`. После пуша нового диджеста и `kubectl rollout restart` — заработало.
|
||
|
||
---
|
||
|
||
### Улучшение: убрать поля API/MQTT из формы входа (v0.1.56)
|
||
|
||
**Анализ:** Пользователь справедливо спросил "ЗАЧЕМ юзеру это вводить?" — адреса `https://sless.kube5s.ru` и `wss://iot.kube5s.ru/mqtt` фиксированы для данного деплоя. Пользователь не должен их трогать.
|
||
|
||
**Решение:** Удалены `<input id="f-api">` и `<input id="f-mqtt">` из формы. В `doLogin()` адреса берутся из хардкода, не из DOM. Форма стала: только поле токена + кнопка "Войти".
|
||
|
||
**Параллельно:** Добавлен блок `<details class="help-block">` внизу страницы устройства — 5 шагов инструкции: Credentials → формат JSON → Эмулятор → Авто → Телеметрия (скоро).
|
||
|
||
---
|
||
|
||
### Ребрендинг: Nubes brand design (v0.1.57)
|
||
|
||
**Задача:** "Оформи чтобы строго, чётко — как на terra.k8c.ru".
|
||
|
||
**Исследование:**
|
||
- Скачал SVG логотипа: `https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg`
|
||
- Логотип залит `#001C34` — это основной Nubes Navy цвет
|
||
- Сайт nubes.ru использует тёмно-синий (#001C34) как бренд-прайм
|
||
|
||
**Палитра:**
|
||
| Переменная | Цвет | Назначение |
|
||
|----------------|------------|------------------------------|
|
||
| brand primary | `#001C34` | Navbar, карточки, логотип |
|
||
| page bg | `#001120` | Фон страницы |
|
||
| card surface | `#001929` | Карточки .card |
|
||
| borders | `#0b2d50` | Границы, разделители |
|
||
| accent | `#1a7fd4` | Кнопки, табы, ссылки |
|
||
| text primary | `#e2ecf6` | Основной текст |
|
||
| text secondary | `#6b8eaa` | Метки, подписи |
|
||
| text muted | `#2d5070` | Отключённые, подсказки |
|
||
|
||
**Изменения в CSS:**
|
||
- Navbar: `background: #001C34`, логотип SVG с `filter: brightness(0) invert(1)` (белый)
|
||
- Badges: прямоугольные (`border-radius: 4px`), UPPERCASE, компактные
|
||
- Кнопки: `font-weight: 600`, `letter-spacing: 0.02em`
|
||
- Таблицы: заголовки `color: #2d5070` — строгие, тихие
|
||
- `.help-num`: квадратные (4px), не круглые
|
||
|
||
**Форма входа:** логотип SVG (инвертированный) вместо `⚡`, подпись `IoT Console` uppercase вместо названия по-русски.
|
||
|
||
---
|
||
|
||
### Favicon (v0.1.58)
|
||
|
||
**Задача:** Иконка вкладки браузера — как у Nubes docs.
|
||
|
||
**Исследование:** `curl https://terra.k8c.ru/docs/nubes/nubes/2.0.2/` → `<link rel="icon" href="30_registry/assets/favicon.png">`
|
||
|
||
**URL:** `https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png`
|
||
|
||
**Решение:** Добавлена одна строка в `<head>`:
|
||
```html
|
||
<link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png">
|
||
```
|
||
|
||
---
|
||
|
||
## Итоговые версии
|
||
|
||
| Версия | Изменение | Коммит |
|
||
|---------|-------------------------------------------------|----------|
|
||
| v0.1.54 | TLS на iot.kube5s.ru, wss://, CORS https | fb6f9d4 |
|
||
| v0.1.55 | Help-блок на странице устройства | e547871 |
|
||
| v0.1.56 | Убраны поля API/MQTT из формы входа | e547871 |
|
||
| v0.1.57 | Nubes brand rebrand — палитра, логотип | 0400f97 |
|
||
| v0.1.58 | Favicon Nubes | 93e87a3 |
|
||
|
||
## Текущее состояние
|
||
|
||
- ✅ `https://iot.kube5s.ru/console` — работает, TLS, Nubes-дизайн, favicon
|
||
- ✅ MQTT: `wss://iot.kube5s.ru/mqtt`
|
||
- ✅ `crypto.subtle` работает (HTTPS)
|
||
- ✅ Форма входа: только токен
|
||
- ✅ Namespace скрыт от пользователя
|
||
- ❌ Телеметрия — заглушка, бэкенд не написан
|
||
|
||
## Следующий шаг
|
||
|
||
Бэкенд телеметрии:
|
||
1. Postgres StatefulSet в namespace `iot`
|
||
2. Tenant provisioning при создании IoTDevice
|
||
3. INSERT в mqtt-bridge
|
||
4. REST API чтения
|
||
5. Вкладка Телеметрия в UI
|