Новые файлы: - doc/architecture/current-v0.2.3.md — актуальная архитектура - doc/api/endpoints-v0.2.3.md — полная документация REST API - doc/deployment-v0.2.3.md — инструкция деплоя v0.2.3 - doc/run-and-test.md — руководство по запуску и E2E тесту Обновлено: - doc/progress.md — секция документации - doc/thinking/2026-04-12.md — лог мышления
411 lines
11 KiB
Markdown
411 lines
11 KiB
Markdown
# 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 <token>
|
||
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 <token>
|
||
```
|
||
|
||
**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 <token>
|
||
```
|
||
|
||
**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 <token>
|
||
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 <token>
|
||
```
|
||
|
||
**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 <token>
|
||
```
|
||
|
||
**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 <ADMIN_STATS_TOKEN>
|
||
```
|
||
|
||
**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"
|
||
```
|