> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md # IoT REST API — Полная документация (v0.2.3) > Дата: 2026-04-12 > Базовый URL: https://iot.kube5s.ru (через Ingress) или http://iot-operator.sless.svc:9090 (из кластера) > Аутентификация: Bearer JWT в заголовке Authorization (authTestMode=true: любой непустой токен) --- ## Содержание 1. [IoT Devices CRUD](#iot-devices-crud) 2. [IoT Telemetry](#iot-telemetry) 3. [MQTT Auth (internal)](#mqtt-auth-internal) 4. [MQTT ACL (internal)](#mqtt-acl-internal) 5. [Admin Stats](#admin-stats) 6. [UI Pages](#ui-pages) 7. [Коды ошибок](#коды-ошибок) --- ## IoT Devices CRUD ### POST /v1/namespaces/{namespace}/iot/devices — Создание устройства Создаёт IoTDevice CRD. Контроллер асинхронно генерирует MQTT credentials (Secret). **Headers:** ``` Authorization: Bearer Content-Type: application/json ``` **Request body:** ```json { "name": "sensor-01", "device_id": "sensor-01", "enabled": true, "metadata": { "model": "DHT22", "location": "room-1" } } ``` | Поле | Тип | Обязательное | Описание | |------|-----|-------------|----------| | name | string | да | Имя k8s объекта IoTDevice (уникальное в namespace) | | device_id | string | да | ID устройства, pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | | enabled | bool | нет | default: true | | metadata | map[string]string | нет | Произвольные метаданные | **Response 201 Created:** ```json { "name": "sensor-01", "namespace": "tenant-abc", "device_id": "sensor-01", "enabled": true, "phase": "", "metadata": {"model": "DHT22", "location": "room-1"}, "created_at": "2026-04-12 14:30:00 UTC" } ``` **Response 409 Conflict:** `{"error": "iot device already exists"}` --- ### GET /v1/namespaces/{namespace}/iot/devices — Список устройств Возвращает все IoTDevice в namespace. **Пароли НЕ включены** (security by design). **Headers:** ``` Authorization: Bearer ``` **Response 200 OK:** ```json [ { "name": "sensor-01", "namespace": "tenant-abc", "device_id": "sensor-01", "enabled": true, "phase": "Active", "mqtt_username": "tenant-abc_sensor-01", "secret_name": "iot-sensor-01", "topic_prefix": "tenant-abc/", "metadata": {"model": "DHT22"}, "created_at": "2026-04-12 14:30:00 UTC" } ] ``` --- ### GET /v1/namespaces/{namespace}/iot/devices/{name} — Получение устройства Возвращает устройство **включая mqtt_password** из Secret. Используется для конфигурации физического устройства. **Headers:** ``` Authorization: Bearer ``` **Response 200 OK:** ```json { "name": "sensor-01", "namespace": "tenant-abc", "device_id": "sensor-01", "enabled": true, "phase": "Active", "mqtt_username": "tenant-abc_sensor-01", "mqtt_password": "a1b2c3d4...hex64chars", "secret_name": "iot-sensor-01", "topic_prefix": "tenant-abc/", "last_connected": "2026-04-12T14:35:00Z", "metadata": {"model": "DHT22"}, "created_at": "2026-04-12 14:30:00 UTC" } ``` **Response 404:** `{"error": "iot device not found"}` > Примечание: mqtt_password будет пустым если Secret ещё не создан (phase=Pending). --- ### PATCH /v1/namespaces/{namespace}/iot/devices/{name} — Обновление устройства Включает/отключает устройство. **Headers:** ``` Authorization: Bearer Content-Type: application/json ``` **Request body:** ```json { "enabled": false } ``` | Поле | Тип | Обязательное | Описание | |------|-----|-------------|----------| | enabled | bool | да | true=Active, false=Disabled | **Response 200 OK:** полный объект устройства (без пароля). **Response 404:** `{"error": "iot device not found"}` --- ### DELETE /v1/namespaces/{namespace}/iot/devices/{name} — Удаление устройства Удаляет IoTDevice CRD. Контроллер через finalizer удаляет Secret каскадно. **Headers:** ``` Authorization: Bearer ``` **Response 204 No Content** (пустое тело) **Response 404:** `{"error": "iot device not found"}` --- ## IoT Telemetry ### GET /v1/namespaces/{namespace}/iot/telemetry — Чтение телеметрии Возвращает записи телеметрии из per-tenant Postgres. **Headers:** ``` Authorization: Bearer ``` **Query parameters:** | Параметр | Тип | Default | Описание | |----------|-----|---------|----------| | device | string | (все) | Фильтр по device_id | | limit | int | 50 | Макс. кол-во записей (max 1000) | **Response 200 OK:** ```json { "items": [ { "id": 1, "device_id": "sensor-01", "payload": {"temperature": 22.5, "humidity": 65}, "received_at": "2026-04-12T14:35:00Z", "created_at": "2026-04-12T14:35:01Z" } ], "count": 1 } ``` **Response 503:** `{"error": "IoT telemetry storage not configured"}` (IOT_PG_DSN не задан) --- ## MQTT Auth (internal) ### POST /internal/mqtt/auth — Аутентификация MQTT клиента Вызывается EMQX при каждом MQTT CONNECT. **Без JWT.** Доступен только из кластера. **Request body (от EMQX):** ```json { "username": "tenant-abc_sensor-01", "password": "a1b2c3d4...hex64chars", "clientid": "mqtt-client-123", "peerhost": "10.0.1.5" } ``` **Логика:** 1. Если username == BridgeUsername → проверить BridgePassword (constant-time) → allow/deny 2. Парсить username по первому "_" → namespace + deviceId 3. Найти Secret `iot-{deviceId}` в namespace 4. `crypto/subtle.ConstantTimeCompare(password, secret["mqtt-password"])` 5. Проверить IoTDevice существует и enabled=true 6. Обновить status.lastConnected (best-effort) 7. Вернуть ACL правила для клиента **Response 200 (allow с ACL):** ```json { "result": "allow", "acl": [ {"permission": "allow", "action": "publish", "topic": "tenant-abc/telemetry/sensor-01"}, {"permission": "allow", "action": "subscribe", "topic": "tenant-abc/telemetry/sensor-01"}, {"permission": "deny", "action": "all", "topic": "#"} ] } ``` **Response 200 (bridge allow):** ```json { "result": "allow" } ``` **Response 200 (deny):** ```json { "result": "deny" } ``` > Всегда HTTP 200. EMQX игнорирует non-200 ответы. --- ## MQTT ACL (internal) ### POST /internal/mqtt/acl — Авторизация pub/sub Вызывается EMQX для каждого publish/subscribe. **Без JWT.** **Request body:** ```json { "username": "tenant-abc_sensor-01", "clientid": "mqtt-client-123", "action": "publish", "topic": "tenant-abc/telemetry/sensor-01" } ``` **Логика:** - Bridge (clientid=sless-iot-bridge): только subscribe → allow. Publish → deny. - Device: action на topic `{ns}/telemetry/{deviceId}` → allow. Всё остальное → deny. **Response 200:** `{"result": "allow"}` или `{"result": "deny"}` --- ## Admin Stats ### GET /iot-admin/stats — Статистика администратора Защищён токеном ADMIN_STATS_TOKEN (env). Не проходит через JWT middleware. **Headers:** ``` Authorization: Bearer ``` **Response 200 OK:** ```json { "collected_at": "2026-04-12T14:40:00Z", "postgres": { "reachable": true, "tenants": [ { "namespace": "tenant-abc", "total_count": 150, "last_1h_count": 42, "last_24h_count": 130, "latest_rows": [...] } ] }, "sqs": { "approximate_messages": 5, "approximate_messages_not_visible": 2 }, "pods": { "iot-mqtt-bridge": { "name": "iot-mqtt-bridge-xxx", "phase": "Running", "ready": true, "restarts": 0, "age": "3h" }, "iot-sqs-consumer": { "name": "iot-sqs-consumer-yyy", "phase": "Running", "ready": true, "restarts": 0, "age": "3h" } } } ``` **Response 401:** `{"error": "unauthorized"}` **Response 503:** `{"error": "admin stats not configured: ADMIN_STATS_TOKEN not set"}` --- ## UI Pages ### GET /console — IoT Консоль HTML-страница (go:embed) для управления устройствами и просмотра телеметрии. Включает MQTT WebSocket клиент для реального времени. ### GET /iot-admin — IoT Admin Panel HTML-страница (go:embed) администратора с графиками и мониторингом. --- ## Коды ошибок | Код | Значение | Когда | |-----|---------|-------| | 200 | OK | Успешные GET, PATCH, MQTT auth/acl | | 201 | Created | Успешный POST (создание устройства) | | 204 | No Content | Успешный DELETE | | 400 | Bad Request | Невалидный JSON, отсутствуют обязательные поля | | 401 | Unauthorized | Невалидный/отсутствующий Bearer token | | 404 | Not Found | Устройство не найдено | | 409 | Conflict | Устройство уже существует | | 500 | Internal Server Error | Ошибка k8s API или БД | | 503 | Service Unavailable | IoTPG не сконфигурирован или AdminToken не задан | --- ## Curl примеры ```bash # Создать устройство curl -X POST https://iot.kube5s.ru/v1/namespaces/test-ns/iot/devices \ -H "Authorization: Bearer test-token" \ -H "Content-Type: application/json" \ -d '{"name":"sensor-01","device_id":"sensor-01","enabled":true}' # Список устройств curl https://iot.kube5s.ru/v1/namespaces/test-ns/iot/devices \ -H "Authorization: Bearer test-token" # Получить устройство с паролем curl https://iot.kube5s.ru/v1/namespaces/test-ns/iot/devices/sensor-01 \ -H "Authorization: Bearer test-token" # Включить/отключить curl -X PATCH https://iot.kube5s.ru/v1/namespaces/test-ns/iot/devices/sensor-01 \ -H "Authorization: Bearer test-token" \ -H "Content-Type: application/json" \ -d '{"enabled":false}' # Удалить curl -X DELETE https://iot.kube5s.ru/v1/namespaces/test-ns/iot/devices/sensor-01 \ -H "Authorization: Bearer test-token" # Телеметрия (последние 100) curl "https://iot.kube5s.ru/v1/namespaces/test-ns/iot/telemetry?limit=100" \ -H "Authorization: Bearer test-token" # Телеметрия по устройству curl "https://iot.kube5s.ru/v1/namespaces/test-ns/iot/telemetry?device=sensor-01&limit=50" \ -H "Authorization: Bearer test-token" ```