refactor: старые артефакты IoT перенесены в legacy/ (git mv, история сохранена); активное дерево очищено, go build OK
This commit is contained in:
@@ -1,412 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (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 <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"
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT API Endpoints
|
||||
|
||||
## Публичные (без auth)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| GET | `/console` | IoT Консоль (HTML SPA) |
|
||||
| GET | `/iot-admin` | Страница администратора (HTML) |
|
||||
|
||||
## Защищённые JWT (/v1/)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| POST | `/v1/namespaces/{ns}/iot/devices` | Создать IoT устройство |
|
||||
| GET | `/v1/namespaces/{ns}/iot/devices` | Список устройств (без паролей) |
|
||||
| GET | `/v1/namespaces/{ns}/iot/devices/{name}` | Устройство + MQTT password |
|
||||
| DELETE | `/v1/namespaces/{ns}/iot/devices/{name}` | Удалить устройство |
|
||||
| PATCH | `/v1/namespaces/{ns}/iot/devices/{name}` | Обновить (enabled) |
|
||||
| GET | `/v1/namespaces/{ns}/iot/telemetry` | Телеметрия (?device=&limit=) |
|
||||
|
||||
## Внутренние (без JWT, только из кластера)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| POST | `/internal/mqtt/auth` | MQTT auth для EMQX |
|
||||
| POST | `/internal/mqtt/acl` | MQTT ACL для EMQX |
|
||||
|
||||
## Администратор (ADMIN_STATS_TOKEN)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| GET | `/iot-admin/stats` | JSON статистика (PG, Kafka lag, pods) |
|
||||
|
||||
---
|
||||
|
||||
## Обновление 2026-04-12: admin stats → SQS
|
||||
|
||||
> Kafka lag заменён на SQS queue stats. Endpoint тот же, формат ответа изменён.
|
||||
|
||||
### `/iot-admin/stats` — актуальный формат ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"sqs": {
|
||||
"queue_url": "http://us-east-1.goaws.com:4100/t-96afe7e9f781f6ca/iot-telemetry",
|
||||
"approximate_messages": "0",
|
||||
"approximate_messages_not_visible": "0"
|
||||
},
|
||||
"sqs_consumer_pods": [...],
|
||||
"pg_tenants": [...]
|
||||
}
|
||||
```
|
||||
|
||||
Вместо `kafka_lag` теперь `sqs.approximate_messages` — количество сообщений в очереди, ожидающих обработки.
|
||||
@@ -1,258 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT Managed Service — Актуальная архитектура (v0.2.3)
|
||||
|
||||
> Дата: 2026-04-12
|
||||
> Образ: naeel/iot-operator:v0.2.3
|
||||
> Кластер: iot-naeel, namespace: sless
|
||||
|
||||
---
|
||||
|
||||
## Путь данных (data flow)
|
||||
|
||||
```
|
||||
IoT Device (MQTT CONNECT)
|
||||
| username="{ns}_{deviceId}", password=hex(64)
|
||||
v
|
||||
EMQX 5.5.1 (Pod emqx, namespace sless)
|
||||
| 1. POST /internal/mqtt/auth -> iot-operator:9090
|
||||
| - Bridge auth: username=iot-bridge-internal -> allow
|
||||
| - Device auth: username={ns}_{deviceId} -> Secret -> compare
|
||||
| 2. POST /internal/mqtt/acl -> iot-operator:9090
|
||||
| - Device: pub/sub только {ns}/telemetry/{deviceId}
|
||||
| - Bridge: sub на +/telemetry/+ (wildcard)
|
||||
v
|
||||
| MQTT PUBLISH -> topic: "{ns}/telemetry/{deviceId}"
|
||||
v
|
||||
iot-mqtt-bridge (Pod, namespace sless)
|
||||
| paho.mqtt.golang, подписка на "+/telemetry/+"
|
||||
| Парсит topic -> namespace (segment 0), deviceId (segment 2)
|
||||
| AWS SDK SQS SendMessage -> shared-SQS
|
||||
v
|
||||
shared-SQS (namespace shared-sqs)
|
||||
| Endpoint: https://qu.kube5s.ru
|
||||
| Tenant: iot-service (ID: t-96afe7e9f781f6ca)
|
||||
| Queue: iot-telemetry
|
||||
v
|
||||
iot-sqs-consumer (Pod, namespace sless)
|
||||
| ReceiveMessage (WaitTimeSeconds=20, long polling)
|
||||
| Парсит envelope -> namespace, device_id, payload
|
||||
| EnsureTenantDB(namespace) -> CREATE DATABASE tenant_{ns}
|
||||
| INSERT INTO iot_telemetry
|
||||
| DeleteMessage (at-least-once)
|
||||
v
|
||||
Managed PostgreSQL 17
|
||||
| Host: postgresqlk8s-master.dc5db45d-....svc.cluster.local
|
||||
| Per-tenant: DATABASE tenant_{namespace}
|
||||
| Таблица: iot_telemetry (id, device_id, payload JSONB, received_at, created_at)
|
||||
v
|
||||
REST API (iot-operator:9090)
|
||||
| GET /v1/namespaces/{ns}/iot/telemetry?device=X&limit=N
|
||||
v
|
||||
Пользователь
|
||||
| IoT Console: https://iot.kube5s.ru/console
|
||||
| WebSocket MQTT: wss://iot.kube5s.ru/mqtt
|
||||
| Terraform: sless_iot_device resource
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Компоненты
|
||||
|
||||
### iot-operator (cmd/iot-operator)
|
||||
|
||||
Единый бинарник: controller-manager + REST API сервер.
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| IoTDevice Controller | Reconcile: создание Secret с MQTT credentials, OwnerReference |
|
||||
| REST API :9090 | CRUD устройств, телеметрия, MQTT auth/acl, admin stats, UI |
|
||||
| Health :8081 | /healthz, /readyz для k8s probes |
|
||||
|
||||
### iot-mqtt-bridge (cmd/mqtt-bridge)
|
||||
|
||||
MQTT subscriber -> SQS producer. Stateless.
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| MQTT broker | tcp://emqx.sless.svc:1883 |
|
||||
| MQTT username | iot-bridge-internal (Secret iot-bridge-credentials) |
|
||||
| MQTT subscription | +/telemetry/+ |
|
||||
| SQS endpoint | https://qu.kube5s.ru |
|
||||
| SQS queue | iot-telemetry |
|
||||
|
||||
Bridge auth (v0.2.3): operator проверяет BridgeUsername/BridgePassword ДО парсинга namespace_deviceId.
|
||||
|
||||
### iot-sqs-consumer (cmd/sqs-consumer)
|
||||
|
||||
SQS consumer -> Postgres writer. Stateless.
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| SQS endpoint | https://qu.kube5s.ru |
|
||||
| SQS queue | iot-telemetry |
|
||||
| Long polling | WaitTimeSeconds=20 |
|
||||
| Postgres | IOT_PG_DSN из Secret |
|
||||
| Семантика | at-least-once (DeleteMessage после INSERT) |
|
||||
|
||||
### EMQX 5.5.1
|
||||
|
||||
MQTT-брокер с HTTP auth backend.
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Образ | emqx/emqx:5.5.1 |
|
||||
| Порты | 1883 (MQTT), 8083 (WebSocket), 18083 (Dashboard) |
|
||||
| Auth | HTTP POST -> iot-operator:9090/internal/mqtt/auth |
|
||||
| ACL | HTTP POST -> iot-operator:9090/internal/mqtt/acl |
|
||||
| Конфиг | HOCON emqx.conf через ConfigMap |
|
||||
|
||||
---
|
||||
|
||||
## CRD: IoTDevice (iot.kube5s.ru/v1alpha1)
|
||||
|
||||
### Spec
|
||||
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|-------------|----------|
|
||||
| deviceId | string | да | Pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ |
|
||||
| enabled | bool | нет | default: true |
|
||||
| metadata | map[string]string | нет | Произвольные метаданные |
|
||||
|
||||
### Status
|
||||
|
||||
| Поле | Описание |
|
||||
|------|----------|
|
||||
| phase | Active / Disabled / Pending / Error |
|
||||
| mqttUsername | {namespace}_{deviceId} |
|
||||
| secretName | iot-{deviceId} |
|
||||
| topicPrefix | {namespace}/ |
|
||||
| message | Сообщение об ошибке |
|
||||
|
||||
### Reconcile logic
|
||||
|
||||
1. Добавить finalizer iot.kube5s.ru/device-cleanup
|
||||
2. Если Secret iot-{deviceId} не существует:
|
||||
- crypto/rand 32 bytes -> hex (64 символа) = пароль
|
||||
- Создать Secret с OwnerReference -> каскадное удаление
|
||||
- Keys: mqtt-username, mqtt-password, device-id
|
||||
3. Status: phase=Active, mqttUsername={ns}_{deviceId}
|
||||
4. Если enabled=false -> phase=Disabled (Secret НЕ удаляется)
|
||||
5. DELETE: finalizer cleanup -> Secret удаляется каскадно
|
||||
|
||||
---
|
||||
|
||||
## Аутентификация
|
||||
|
||||
### REST API (/v1/)
|
||||
|
||||
- Middleware: Bearer JWT token
|
||||
- authTestMode = true (текущий): любой непустой Bearer token принимается
|
||||
- authTestMode = false (prod): JWT decode -> sub -> SHA256 -> namespace mapping
|
||||
|
||||
### MQTT Auth (/internal/mqtt/auth)
|
||||
|
||||
1. Если username == BridgeUsername -> проверить BridgePassword -> allow/deny
|
||||
2. Иначе: парсить username по первому "_" -> namespace + deviceId
|
||||
3. Найти Secret iot-{deviceId} в namespace
|
||||
4. crypto/subtle.ConstantTimeCompare(password, secret.mqtt-password)
|
||||
5. Всегда HTTP 200, body: {"result": "allow"} или {"result": "deny"}
|
||||
|
||||
### MQTT ACL (/internal/mqtt/acl)
|
||||
|
||||
- Bridge (clientid=sless-iot-bridge): allow subscribe +/telemetry/+
|
||||
- Device: allow pub/sub только {namespace}/telemetry/{deviceId}
|
||||
- Всё остальное: deny
|
||||
|
||||
---
|
||||
|
||||
## Хранение данных
|
||||
|
||||
### PostgreSQL (managed)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Версия | PG 17 |
|
||||
| Host | postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local |
|
||||
| User | super |
|
||||
| Master DB | sqsdb |
|
||||
|
||||
Per-tenant изоляция: отдельная DATABASE tenant_{namespace}.
|
||||
|
||||
Таблица iot_telemetry:
|
||||
- id SERIAL PRIMARY KEY
|
||||
- device_id TEXT NOT NULL
|
||||
- payload JSONB NOT NULL
|
||||
- received_at TIMESTAMPTZ
|
||||
- created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
|
||||
### shared-SQS
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Endpoint | https://qu.kube5s.ru |
|
||||
| Tenant | iot-service (t-96afe7e9f781f6ca) |
|
||||
| Queue | iot-telemetry |
|
||||
| Протокол | AWS SQS API compatible |
|
||||
|
||||
---
|
||||
|
||||
## Docker образ
|
||||
|
||||
- Registry: Docker Hub naeel/iot-operator
|
||||
- Базовый: gcr.io/distroless/static:nonroot
|
||||
- Содержит 3 бинарника: /iot-operator, /mqtt-bridge, /sqs-consumer
|
||||
- Выбор бинарника через command в Deployment YAML
|
||||
|
||||
---
|
||||
|
||||
## Сетевая схема
|
||||
|
||||
```
|
||||
Internet
|
||||
|
|
||||
v
|
||||
nginx-ingress (namespace ingress)
|
||||
| iot.kube5s.ru/mqtt -> emqx-ws:8083 (WebSocket)
|
||||
| iot.kube5s.ru/console -> iot-operator:9090 (UI)
|
||||
| iot.kube5s.ru/iot-admin -> iot-operator:9090 (Admin UI)
|
||||
v
|
||||
namespace sless:
|
||||
emqx:1883 <-> iot-mqtt-bridge (MQTT)
|
||||
emqx:1883 <- IoT devices (MQTT)
|
||||
iot-operator:9090 <- emqx (auth/acl HTTP)
|
||||
iot-operator:9090 <- users (REST API)
|
||||
iot-mqtt-bridge -> shared-sqs (HTTPS, SQS API)
|
||||
iot-sqs-consumer <- shared-sqs (HTTPS, SQS API)
|
||||
iot-sqs-consumer -> managed-postgres (TCP 5432)
|
||||
iot-operator -> managed-postgres (TCP 5432, telemetry GET)
|
||||
iot-operator -> k8s API (CRD watch, Secret CRUD)
|
||||
|
||||
namespace shared-sqs:
|
||||
shared-sqs:9324 (SQS API)
|
||||
Ingress: qu.kube5s.ru -> shared-sqs
|
||||
|
||||
namespace dc5db45d-...:
|
||||
postgresqlk8s-0 (PG 17 managed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Секреты (namespace sless)
|
||||
|
||||
| Secret | Ключи | Используется |
|
||||
|--------|-------|-------------|
|
||||
| iot-bridge-credentials | MQTT_USERNAME, MQTT_PASSWORD | iot-operator, iot-mqtt-bridge |
|
||||
| iot-sqs-credentials | SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY | iot-mqtt-bridge, iot-sqs-consumer |
|
||||
| iot-postgres-secret | IOT_PG_DSN | iot-sqs-consumer |
|
||||
|
||||
---
|
||||
|
||||
## Эволюция архитектуры
|
||||
|
||||
| Версия | Дата | Message bus | Postgres | Кластер |
|
||||
|--------|------|------------|----------|---------|
|
||||
| v0.1.50 | 2026-04-04 | RabbitMQ (AMQP) | emptyDir PVC | sless (общий) |
|
||||
| v0.1.68 | 2026-04-06 | Kafka (segmentio/kafka-go) | emptyDir PVC | sless (общий) |
|
||||
| v0.2.0 | 2026-04-12 | shared-SQS (AWS SDK) | Managed PG 17 | iot-naeel (новый) |
|
||||
| v0.2.3 | 2026-04-12 | shared-SQS (AWS SDK) | Managed PG 17 | iot-naeel |
|
||||
@@ -1,236 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT Managed Service — Архитектура
|
||||
|
||||
## Общая схема
|
||||
|
||||
```
|
||||
IoT Device (MQTT CONNECT)
|
||||
│ username="{ns}_{deviceId}", password=hex
|
||||
▼
|
||||
EMQX 5.5.1 (sless/emqx)
|
||||
│ POST /internal/mqtt/auth → iot-operator:9090
|
||||
│ POST /internal/mqtt/acl → iot-operator:9090
|
||||
▼
|
||||
│ MQTT PUBLISH → topic: "{ns}/telemetry/{deviceId}"
|
||||
▼
|
||||
iot-mqtt-bridge (cmd/mqtt-bridge)
|
||||
│ подписка на "+/telemetry/+"
|
||||
│ → Kafka topic "iot.telemetry"
|
||||
▼
|
||||
Kafka
|
||||
▼
|
||||
iot-kafka-consumer (cmd/kafka-consumer)
|
||||
│ consumer group "iot-pg-consumer"
|
||||
│ → per-tenant Postgres DB
|
||||
▼
|
||||
IoT Postgres (iot-postgres.sless.svc)
|
||||
│ DB: tenant_{namespace}
|
||||
│ Table: iot_telemetry
|
||||
▼
|
||||
REST API (iot-operator:9090)
|
||||
│ GET /v1/namespaces/{ns}/iot/telemetry
|
||||
▼
|
||||
Пользователь (Terraform / UI Console)
|
||||
```
|
||||
|
||||
## Компоненты
|
||||
|
||||
| Компонент | Бинарник | Порт | Назначение |
|
||||
|-----------|----------|------|------------|
|
||||
| iot-operator | cmd/iot-operator | :9090 (API), :8081 (health) | Controller-manager + REST API |
|
||||
| mqtt-bridge | cmd/mqtt-bridge | — | MQTT→Kafka bridge |
|
||||
| kafka-consumer | cmd/kafka-consumer | — | Kafka→Postgres pipeline |
|
||||
|
||||
## CRD
|
||||
|
||||
- **IoTDevice** (`iot.kube5s.ru/v1alpha1`)
|
||||
- Создание: пользователь через API / Terraform
|
||||
- Controller генерирует MQTT credentials → k8s Secret
|
||||
- Secret удаляется каскадно через OwnerReference
|
||||
|
||||
## Хранение
|
||||
|
||||
- **IoT Postgres** — отдельный от sless PG
|
||||
- Management DB: `iot_platform` (таблица `tenant_credentials`)
|
||||
- Per-tenant DB: `tenant_{namespace}` (таблица `iot_telemetry`)
|
||||
|
||||
## Аутентификация
|
||||
|
||||
- REST API (/v1/): Bearer JWT (middleware.Auth)
|
||||
- MQTT Auth (/internal/): вызывается EMQX, без JWT
|
||||
- Admin (/iot-admin/stats): ADMIN_STATS_TOKEN
|
||||
|
||||
## Docker образ
|
||||
|
||||
- `naeel/iot-operator` (Docker Hub)
|
||||
- Все 3 бинарника в одном образе
|
||||
- `command: ["/iot-operator"]` или `["/mqtt-bridge"]` или `["/kafka-consumer"]`
|
||||
|
||||
---
|
||||
|
||||
## Актуальная архитектура (2026-04-12)
|
||||
|
||||
> Kafka заменён на shared-SQS. Postgres заменён на managed. Новый кластер iot-naeel.
|
||||
|
||||
### Текущая схема
|
||||
|
||||
```
|
||||
IoT Device (MQTT CONNECT)
|
||||
│ username="{ns}_{deviceId}", password=hex
|
||||
▼
|
||||
EMQX 5.5.1 (sless/emqx)
|
||||
│ POST /internal/mqtt/auth → iot-operator:9090
|
||||
│ POST /internal/mqtt/acl → iot-operator:9090
|
||||
▼
|
||||
│ MQTT PUBLISH → topic: "{ns}/telemetry/{deviceId}"
|
||||
▼
|
||||
iot-mqtt-bridge (cmd/mqtt-bridge)
|
||||
│ подписка на "+/telemetry/+"
|
||||
│ → AWS SDK SQS SendMessage → shared-SQS (https://qu.kube5s.ru)
|
||||
│ очередь: "iot-telemetry" (tenant: iot-service, id: t-96afe7e9f781f6ca)
|
||||
▼
|
||||
shared-SQS (namespace shared-sqs)
|
||||
│ AWS SQS-совместимый, multi-tenant
|
||||
▼
|
||||
iot-sqs-consumer (cmd/sqs-consumer)
|
||||
│ ReceiveMessage (WaitTimeSeconds=20, long polling)
|
||||
│ DeleteMessage после успешной записи (at-least-once)
|
||||
│ → per-tenant Postgres DB
|
||||
▼
|
||||
Managed PostgreSQL 17 (namespace dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5)
|
||||
│ Host: postgresqlk8s-master.dc5db45d-....svc.cluster.local
|
||||
│ User: super, DB: sqsdb
|
||||
│ Per-tenant DB: tenant_{namespace}, Table: iot_telemetry
|
||||
▼
|
||||
REST API (iot-operator:9090)
|
||||
│ GET /v1/namespaces/{ns}/iot/telemetry
|
||||
▼
|
||||
Пользователь (Terraform / UI Console)
|
||||
│ wss://iot.kube5s.ru/mqtt (WebSocket через Ingress)
|
||||
│ https://iot.kube5s.ru/console (UI)
|
||||
```
|
||||
|
||||
### Компоненты (актуальные)
|
||||
|
||||
| Компонент | Бинарник | Порт | Назначение |
|
||||
|-----------|----------|------|------------|
|
||||
| iot-operator | cmd/iot-operator | :9090 (API), :8080 (metrics), :8081 (health) | Controller-manager + REST API |
|
||||
| mqtt-bridge | cmd/mqtt-bridge | — | MQTT → shared-SQS bridge |
|
||||
| sqs-consumer | cmd/sqs-consumer | — | shared-SQS → Postgres pipeline |
|
||||
|
||||
### Инфраструктура
|
||||
|
||||
| Сервис | Тип | Namespace | Описание |
|
||||
|--------|-----|-----------|----------|
|
||||
| EMQX 5.5.1 | В кластере | sless | MQTT-брокер, HTTP auth/acl → iot-operator |
|
||||
| shared-SQS | В кластере | shared-sqs | AWS SQS-совместимая очередь, endpoint https://qu.kube5s.ru |
|
||||
| PostgreSQL 17 | Managed (оператор) | dc5db45d-... | per-tenant DB, тот же инстанс что и SQS billing |
|
||||
| cert-manager | В кластере | cert-manager | TLS сертификаты для iot.kube5s.ru |
|
||||
| nginx-ingress | В кластере | ingress | WSS/HTTPS проксирование |
|
||||
|
||||
### Секреты в namespace sless
|
||||
|
||||
| Secret | Ключи | Источник |
|
||||
|--------|-------|----------|
|
||||
| iot-sqs-credentials | SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY | shared-SQS tenant iot-service |
|
||||
| iot-postgres-secret | POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, IOT_PG_DSN | Managed PG |
|
||||
| iot-bridge-credentials | MQTT_USERNAME, MQTT_PASSWORD | Для mqtt-bridge → EMQX |
|
||||
|
||||
### Docker образ (актуальный)
|
||||
|
||||
- `naeel/iot-operator:v0.2.0` (Docker Hub)
|
||||
- Все 3 бинарника в одном образе
|
||||
- `command: ["/iot-operator"]` или `["/mqtt-bridge"]` или `["/sqs-consumer"]`
|
||||
- Базовый: `gcr.io/distroless/static:nonroot`
|
||||
|
||||
### Кластер
|
||||
|
||||
- Имя: `iot-naeel`
|
||||
- API: `https://185.247.187.149:6443`
|
||||
- Ingress IP: `185.247.187.151`
|
||||
- DNS: `iot.kube5s.ru → 185.247.187.147`
|
||||
|
||||
---
|
||||
|
||||
## Актуальная архитектура (2026-04-12)
|
||||
|
||||
> Kafka заменён на shared-SQS. Postgres заменён на managed. Новый кластер iot-naeel.
|
||||
|
||||
### Текущая схема
|
||||
|
||||
```
|
||||
IoT Device (MQTT CONNECT)
|
||||
│ username="{ns}_{deviceId}", password=hex
|
||||
▼
|
||||
EMQX 5.5.1 (sless/emqx)
|
||||
│ POST /internal/mqtt/auth → iot-operator:9090
|
||||
│ POST /internal/mqtt/acl → iot-operator:9090
|
||||
▼
|
||||
│ MQTT PUBLISH → topic: "{ns}/telemetry/{deviceId}"
|
||||
▼
|
||||
iot-mqtt-bridge (cmd/mqtt-bridge)
|
||||
│ подписка на "+/telemetry/+"
|
||||
│ → AWS SDK SQS SendMessage → shared-SQS (https://qu.kube5s.ru)
|
||||
│ очередь: "iot-telemetry" (tenant: iot-service, id: t-96afe7e9f781f6ca)
|
||||
▼
|
||||
shared-SQS (namespace shared-sqs)
|
||||
│ AWS SQS-совместимый, multi-tenant
|
||||
▼
|
||||
iot-sqs-consumer (cmd/sqs-consumer)
|
||||
│ ReceiveMessage (WaitTimeSeconds=20, long polling)
|
||||
│ DeleteMessage после успешной записи (at-least-once)
|
||||
│ → per-tenant Postgres DB
|
||||
▼
|
||||
Managed PostgreSQL 17 (namespace dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5)
|
||||
│ Host: postgresqlk8s-master.dc5db45d-....svc.cluster.local
|
||||
│ User: super, DB: sqsdb
|
||||
│ Per-tenant DB: tenant_{namespace}, Table: iot_telemetry
|
||||
▼
|
||||
REST API (iot-operator:9090)
|
||||
│ GET /v1/namespaces/{ns}/iot/telemetry
|
||||
▼
|
||||
Пользователь (Terraform / UI Console)
|
||||
│ wss://iot.kube5s.ru/mqtt (WebSocket через Ingress)
|
||||
│ https://iot.kube5s.ru/console (UI)
|
||||
```
|
||||
|
||||
### Компоненты (актуальные)
|
||||
|
||||
| Компонент | Бинарник | Порт | Назначение |
|
||||
|-----------|----------|------|------------|
|
||||
| iot-operator | cmd/iot-operator | :9090 (API), :8080 (metrics), :8081 (health) | Controller-manager + REST API |
|
||||
| mqtt-bridge | cmd/mqtt-bridge | — | MQTT → shared-SQS bridge |
|
||||
| sqs-consumer | cmd/sqs-consumer | — | shared-SQS → Postgres pipeline |
|
||||
|
||||
### Инфраструктура
|
||||
|
||||
| Сервис | Тип | Namespace | Описание |
|
||||
|--------|-----|-----------|----------|
|
||||
| EMQX 5.5.1 | В кластере | sless | MQTT-брокер, HTTP auth/acl → iot-operator |
|
||||
| shared-SQS | В кластере | shared-sqs | AWS SQS-совместимая очередь, endpoint https://qu.kube5s.ru |
|
||||
| PostgreSQL 17 | Managed (оператор) | dc5db45d-... | per-tenant DB, тот же инстанс что и SQS billing |
|
||||
| cert-manager | В кластере | cert-manager | TLS сертификаты для iot.kube5s.ru |
|
||||
| nginx-ingress | В кластере | ingress | WSS/HTTPS проксирование |
|
||||
|
||||
### Секреты в namespace sless
|
||||
|
||||
| Secret | Ключи | Источник |
|
||||
|--------|-------|----------|
|
||||
| iot-sqs-credentials | SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY | shared-SQS tenant iot-service |
|
||||
| iot-postgres-secret | POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, IOT_PG_DSN | Managed PG |
|
||||
| iot-bridge-credentials | MQTT_USERNAME, MQTT_PASSWORD | Для mqtt-bridge → EMQX |
|
||||
|
||||
### Docker образ (актуальный)
|
||||
|
||||
- `naeel/iot-operator:v0.2.0` (Docker Hub)
|
||||
- Все 3 бинарника в одном образе
|
||||
- `command: ["/iot-operator"]` или `["/mqtt-bridge"]` или `["/sqs-consumer"]`
|
||||
- Базовый: `gcr.io/distroless/static:nonroot`
|
||||
|
||||
### Кластер
|
||||
|
||||
- Имя: `iot-naeel`
|
||||
- API: `https://185.247.187.149:6443`
|
||||
- Ingress IP: `185.247.187.151`
|
||||
- DNS: `iot.kube5s.ru → 185.247.187.147`
|
||||
@@ -1,79 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# Решение: замена Kafka на shared-SQS
|
||||
|
||||
**Дата:** 2026-04-12
|
||||
**Статус:** в работе
|
||||
**Ветка:** `feature/replace-kafka-with-sqs`
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
IoT-сервис использует Kafka (библиотека `segmentio/kafka-go`) как промежуточную очередь между MQTT-bridge и Postgres consumer.
|
||||
Kafka — тяжёлый компонент: требует отдельный деплоймент в кластере (`deployments/k8s/kafka.yaml`), ZooKeeper/KRaft, настройку топиков, партиций.
|
||||
|
||||
У нас есть собственный сервис **shared-SQS** — AWS SQS-совместимая очередь сообщений:
|
||||
- Репа: https://gitea.services.ngcloud.ru/Nail/shared-SQS
|
||||
- Endpoint: `https://qu.kube5s.ru`
|
||||
- 17 SQS-операций, multi-tenant, Redis persistence, billing
|
||||
- Работает через стандартные AWS SDK
|
||||
|
||||
RabbitMQ в коде IoT **не используется** (только в старых документах как план MVP).
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
Заменить Kafka → shared-SQS во всём IoT pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Что меняется
|
||||
|
||||
### 1. mqtt-bridge (`cmd/mqtt-bridge/main.go`)
|
||||
- **Было:** `kafka.Writer` → `WriteMessages()` в топик `iot.telemetry`
|
||||
- **Стало:** AWS SDK Go v2 → `sqs.SendMessage()` в очередь `iot-telemetry`
|
||||
- Env: `KAFKA_BROKERS` → `SQS_ENDPOINT`, `SQS_ACCESS_KEY`, `SQS_SECRET_KEY`, `SQS_QUEUE_NAME`
|
||||
|
||||
### 2. kafka-consumer → sqs-consumer (`cmd/kafka-consumer/` → `cmd/sqs-consumer/`)
|
||||
- **Было:** `kafka.NewReader` с consumer group, `FetchMessage()` + `CommitMessages()`
|
||||
- **Стало:** polling loop: `sqs.ReceiveMessage(WaitTimeSeconds=20)` + `sqs.DeleteMessage()`
|
||||
- Переименовать директорию и бинарник
|
||||
|
||||
### 3. admin stats handler (`internal/api/handler/iot_admin_stats_handler.go`)
|
||||
- **Было:** Kafka lag (ListOffsets + OffsetFetch)
|
||||
- **Стало:** `sqs.GetQueueAttributes(ApproximateNumberOfMessages, ApproximateNumberOfMessagesNotVisible)`
|
||||
- Env: `KAFKA_BROKERS` → SQS credentials в handler
|
||||
|
||||
### 4. go.mod
|
||||
- Убрать: `github.com/segmentio/kafka-go`
|
||||
- Добавить: `github.com/aws/aws-sdk-go-v2`, `github.com/aws/aws-sdk-go-v2/service/sqs`
|
||||
|
||||
### 5. Deployments
|
||||
- Удалить: `deployments/k8s/kafka.yaml`
|
||||
- Обновить: `deployments/k8s/iot-mqtt-bridge.yaml` — новые env vars
|
||||
- Обновить: `deployments/k8s/iot-kafka-consumer.yaml` → `iot-sqs-consumer.yaml`
|
||||
|
||||
### 6. Dockerfile / Makefile
|
||||
- Переименовать бинарник `kafka-consumer` → `sqs-consumer`
|
||||
|
||||
---
|
||||
|
||||
## Плюсы
|
||||
|
||||
- Убираем Kafka из кластера (экономия ресурсов)
|
||||
- Используем свой managed сервис (единая инфраструктура)
|
||||
- AWS SDK — стандартная библиотека, код проще
|
||||
- Billing и мониторинг из коробки в shared-SQS
|
||||
|
||||
## Риски
|
||||
|
||||
- SQS — pull-based (polling latency до 20s long poll vs Kafka push). Для IoT телеметрии приемлемо.
|
||||
- At-least-once delivery — нужно учитывать idempotency (уже есть в текущем коде: INSERT ON CONFLICT)
|
||||
|
||||
---
|
||||
|
||||
## SQS endpoint
|
||||
|
||||
Пока используем публичный `https://qu.kube5s.ru`. Если есть внутрикластерный сервис — обновим.
|
||||
@@ -1,43 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# Решение: Вынос IoT в отдельную репу
|
||||
|
||||
**Дата:** 2026-04-12
|
||||
**Статус:** Принято и реализовано
|
||||
|
||||
## Контекст
|
||||
|
||||
IoT managed service был частью sless (основной serverless operator).
|
||||
Код IoT жил в нескольких местах:
|
||||
- `iot/` — CRD types, controller, cmd (bridge, consumer)
|
||||
- `internal/storage/iotpg/` — Postgres store
|
||||
- `internal/api/handler/iot_*.go` — REST handlers
|
||||
- `internal/api/ui/iot-*.html` — UI
|
||||
- `main.go`, `Dockerfile` — IoT интегрирован в основной бинарник
|
||||
|
||||
## Проблемы
|
||||
|
||||
1. IoT и sless — разные домены с разными циклами разработки
|
||||
2. Сборка sless включала IoT — лишние зависимости (Kafka, MQTT)
|
||||
3. Деплой любого IoT изменения требовал пересборки всего sless
|
||||
|
||||
## Решение
|
||||
|
||||
Вынести IoT в отдельную репу `gitea.services.ngcloud.ru/Nail/IoT`:
|
||||
- Свой Go модуль, go.mod, Dockerfile
|
||||
- 3 бинарника в одном образе (`naeel/iot-operator`)
|
||||
- Свой controller-manager + REST API (cmd/iot-operator)
|
||||
- Независимый CI/CD цикл
|
||||
|
||||
## Что перенесено
|
||||
|
||||
- CRD types, controller, mqtt-bridge, kafka-consumer — as-is
|
||||
- Handler struct упрощён (убраны S3, PG от sless)
|
||||
- Router — только IoT маршруты
|
||||
- Middleware (auth, logging) — скопированы как есть
|
||||
- K8s manifests, CRD YAML, документация, примеры
|
||||
|
||||
## Риски
|
||||
|
||||
- IoT код в sless нужно будет убрать (или оставить заглушки)
|
||||
- K8s manifests могут требовать обновления (новое имя образа)
|
||||
@@ -1,297 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT Managed Service — Актуальный деплой (v0.2.3)
|
||||
|
||||
> Дата: 2026-04-12
|
||||
> Образ: `naeel/iot-operator:v0.2.3`
|
||||
> Ветка: `feature/replace-kafka-with-sqs`
|
||||
> Кластер: `iot-naeel`
|
||||
> Автор: GitHub Copilot (Claude Opus 4.6)
|
||||
|
||||
---
|
||||
|
||||
## Обзор
|
||||
|
||||
Три бинарника из одного Docker-образа `naeel/iot-operator:v0.2.3`:
|
||||
|
||||
| Бинарник | Deployment | Назначение |
|
||||
|----------|-----------|------------|
|
||||
| `/iot-operator` | iot-operator | Controller-manager (IoTDevice CRD) + REST API :9090 |
|
||||
| `/mqtt-bridge` | iot-mqtt-bridge | MQTT (EMQX) → shared-SQS bridge |
|
||||
| `/sqs-consumer` | iot-sqs-consumer | shared-SQS → Postgres pipeline |
|
||||
|
||||
Плюс:
|
||||
- EMQX 5.5.1 — MQTT-брокер (отдельный образ `emqx/emqx:5.5.1`)
|
||||
|
||||
---
|
||||
|
||||
## Кластер
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Имя | `iot-naeel` |
|
||||
| API | `https://185.247.187.149:6443` |
|
||||
| Ingress IP | `185.247.187.151` |
|
||||
| DNS | `iot.kube5s.ru → 185.247.187.147` |
|
||||
| Namespace | `sless` |
|
||||
| kubeconfig | `~/.kube/config` на ВМ (токен 24ч, обновлять через auth.k8s.ngcloud.ru) |
|
||||
|
||||
---
|
||||
|
||||
## Секреты (namespace sless)
|
||||
|
||||
Перед первым деплоем создать 3 секрета:
|
||||
|
||||
### 1. iot-bridge-credentials (MQTT bridge → EMQX)
|
||||
|
||||
```bash
|
||||
kubectl create secret generic iot-bridge-credentials -n sless \
|
||||
--from-literal=MQTT_USERNAME="iot-bridge-internal" \
|
||||
--from-literal=MQTT_PASSWORD="<пароль bridge>"
|
||||
```
|
||||
|
||||
Используется:
|
||||
- **iot-operator** — для проверки bridge auth в `/internal/mqtt/auth` (env: MQTT_BRIDGE_USERNAME, MQTT_BRIDGE_PASSWORD)
|
||||
- **iot-mqtt-bridge** — для подключения к EMQX (envFrom: secretRef)
|
||||
|
||||
### 2. iot-sqs-credentials (shared-SQS)
|
||||
|
||||
```bash
|
||||
kubectl create secret generic iot-sqs-credentials -n sless \
|
||||
--from-literal=SQS_ENDPOINT="https://qu.kube5s.ru" \
|
||||
--from-literal=SQS_ACCESS_KEY="SSAK-a9964f2723bc6d347f48d153" \
|
||||
--from-literal=SQS_SECRET_KEY="<secret_key>"
|
||||
```
|
||||
|
||||
Используется:
|
||||
- **iot-mqtt-bridge** — для SendMessage
|
||||
- **iot-sqs-consumer** — для ReceiveMessage + DeleteMessage
|
||||
|
||||
### 3. iot-postgres-secret (Managed PostgreSQL)
|
||||
|
||||
```bash
|
||||
kubectl create secret generic iot-postgres-secret -n sless \
|
||||
--from-literal=IOT_PG_DSN="postgres://super:<password>@postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local:5432/sqsdb?sslmode=disable"
|
||||
```
|
||||
|
||||
Используется:
|
||||
- **iot-sqs-consumer** — для записи телеметрии в per-tenant DB
|
||||
|
||||
---
|
||||
|
||||
## Процедура деплоя (пошагово)
|
||||
|
||||
### Все команды выполнять на ВМ через SSH
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/id_ed25519 naeel@5.172.178.213
|
||||
cd /home/naeel/terra/IoT
|
||||
```
|
||||
|
||||
### 0. Сборка образа (если код менялся)
|
||||
|
||||
```bash
|
||||
docker build --no-cache -t naeel/iot-operator:v0.2.3 .
|
||||
docker push naeel/iot-operator:v0.2.3
|
||||
```
|
||||
|
||||
### 1. CRD (один раз, cluster-wide)
|
||||
|
||||
```bash
|
||||
kubectl apply -f config/crd/bases/iot.kube5s.ru_iotdevices.yaml
|
||||
```
|
||||
|
||||
### 2. EMQX
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/emqx.yaml
|
||||
kubectl apply -f deployments/k8s/emqx-ws-ingress.yaml
|
||||
```
|
||||
|
||||
### 3. Postgres secret (один раз)
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/iot-postgres.yaml
|
||||
```
|
||||
|
||||
### 4. SQS secret (один раз)
|
||||
|
||||
```bash
|
||||
kubectl get secret iot-sqs-credentials -n sless
|
||||
# Если нет — создать (см. секцию Секреты выше)
|
||||
```
|
||||
|
||||
### 5. Bridge credentials (один раз)
|
||||
|
||||
```bash
|
||||
kubectl get secret iot-bridge-credentials -n sless
|
||||
# Если нет — создать (см. секцию Секреты выше)
|
||||
```
|
||||
|
||||
### 6. Operator
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/iot-operator.yaml
|
||||
kubectl rollout status deployment/iot-operator -n sless
|
||||
```
|
||||
|
||||
### 7. MQTT Bridge
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml
|
||||
kubectl rollout status deployment/iot-mqtt-bridge -n sless
|
||||
```
|
||||
|
||||
### 8. SQS Consumer
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/iot-sqs-consumer.yaml
|
||||
kubectl rollout status deployment/iot-sqs-consumer -n sless
|
||||
```
|
||||
|
||||
### 9. Проверка
|
||||
|
||||
```bash
|
||||
kubectl get pods -n sless
|
||||
# Ожидаемый результат:
|
||||
# emqx-xxx 1/1 Running
|
||||
# iot-operator-xxx 1/1 Running
|
||||
# iot-mqtt-bridge-xxx 1/1 Running
|
||||
# iot-sqs-consumer-xxx 1/1 Running
|
||||
|
||||
kubectl logs -n sless deployment/iot-operator --tail=5
|
||||
kubectl logs -n sless deployment/iot-mqtt-bridge --tail=5
|
||||
kubectl logs -n sless deployment/iot-sqs-consumer --tail=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обновление образа (rollout)
|
||||
|
||||
```bash
|
||||
# 1. Собрать новый образ
|
||||
docker build --no-cache -t naeel/iot-operator:v0.2.4 .
|
||||
docker push naeel/iot-operator:v0.2.4
|
||||
|
||||
# 2. Обновить тег во ВСЕХ трёх YAML
|
||||
sed -i 's/v0.2.3/v0.2.4/g' deployments/k8s/iot-operator.yaml \
|
||||
deployments/k8s/iot-mqtt-bridge.yaml \
|
||||
deployments/k8s/iot-sqs-consumer.yaml
|
||||
|
||||
# 3. Применить
|
||||
kubectl apply -f deployments/k8s/iot-operator.yaml
|
||||
kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml
|
||||
kubectl apply -f deployments/k8s/iot-sqs-consumer.yaml
|
||||
|
||||
# 4. Проверить
|
||||
kubectl get pods -n sless -w
|
||||
```
|
||||
|
||||
**ВАЖНО:** Если образ с тем же тегом — Kubernetes не скачает новый (кэш на нодах). Всегда менять тег.
|
||||
|
||||
---
|
||||
|
||||
## Компоненты: env vars
|
||||
|
||||
### iot-operator
|
||||
|
||||
| Переменная | Значение | Источник |
|
||||
|-----------|----------|----------|
|
||||
| MQTT_BRIDGE_USERNAME | iot-bridge-internal | Secret iot-bridge-credentials (key: MQTT_USERNAME) |
|
||||
| MQTT_BRIDGE_PASSWORD | (пароль) | Secret iot-bridge-credentials (key: MQTT_PASSWORD) |
|
||||
|
||||
### iot-mqtt-bridge
|
||||
|
||||
| Переменная | Значение | Источник |
|
||||
|-----------|----------|----------|
|
||||
| MQTT_BROKER_URL | tcp://emqx.sless.svc:1883 | YAML env |
|
||||
| SQS_QUEUE_NAME | iot-telemetry | YAML env |
|
||||
| SQS_REGION | us-east-1 | YAML env |
|
||||
| MQTT_USERNAME | iot-bridge-internal | Secret iot-bridge-credentials |
|
||||
| MQTT_PASSWORD | (пароль) | Secret iot-bridge-credentials |
|
||||
| SQS_ENDPOINT | https://qu.kube5s.ru | Secret iot-sqs-credentials |
|
||||
| SQS_ACCESS_KEY | SSAK-... | Secret iot-sqs-credentials |
|
||||
| SQS_SECRET_KEY | (secret) | Secret iot-sqs-credentials |
|
||||
|
||||
### iot-sqs-consumer
|
||||
|
||||
| Переменная | Значение | Источник |
|
||||
|-----------|----------|----------|
|
||||
| SQS_QUEUE_NAME | iot-telemetry | YAML env |
|
||||
| SQS_REGION | us-east-1 | YAML env |
|
||||
| SQS_ENDPOINT | https://qu.kube5s.ru | Secret iot-sqs-credentials |
|
||||
| SQS_ACCESS_KEY | SSAK-... | Secret iot-sqs-credentials |
|
||||
| SQS_SECRET_KEY | (secret) | Secret iot-sqs-credentials |
|
||||
| IOT_PG_DSN | postgres://... | Secret iot-postgres-secret |
|
||||
|
||||
---
|
||||
|
||||
## Managed PostgreSQL
|
||||
|
||||
| Параметр | Значение |
|
||||
|---------|----------|
|
||||
| Версия | PostgreSQL 17 |
|
||||
| Host | postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local |
|
||||
| Port | 5432 |
|
||||
| User | super |
|
||||
| DB | sqsdb |
|
||||
| Тип | Managed (оператор в другом namespace) |
|
||||
|
||||
Per-tenant изоляция: для каждого namespace создаётся отдельная DATABASE `tenant_{namespace}` с таблицей `iot_telemetry`.
|
||||
|
||||
---
|
||||
|
||||
## shared-SQS (очередь)
|
||||
|
||||
| Параметр | Значение |
|
||||
|---------|----------|
|
||||
| Endpoint | https://qu.kube5s.ru |
|
||||
| Tenant | iot-service (ID: t-96afe7e9f781f6ca) |
|
||||
| Очередь | iot-telemetry |
|
||||
| Протокол | AWS SQS API (AWS SDK compatible) |
|
||||
|
||||
---
|
||||
|
||||
## Версии образов (история)
|
||||
|
||||
| Версия | Дата | Изменения |
|
||||
|--------|------|-----------|
|
||||
| v0.2.3 | 2026-04-12 | Bridge auth fix (BridgeUsername/BridgePassword), debug logs |
|
||||
| v0.2.2 | 2026-04-12 | Bridge auth (первая попытка, проблема с табами) |
|
||||
| v0.2.1 | 2026-04-12 | Bridge auth (проблема IsSuperuser field) |
|
||||
| v0.2.0 | 2026-04-12 | Kafka->SQS, managed PG, новый кластер iot-naeel |
|
||||
| v0.1.69 | 2026-04-06 | Kafka Async write fix (1000/1000 тест) |
|
||||
| v0.1.68 | 2026-04-06 | Kafka pipeline (bridge->Kafka->consumer->Postgres) |
|
||||
| v0.1.50 | 2026-04-04 | Первый IoT: controller + API + mqtt-bridge (RabbitMQ) |
|
||||
|
||||
---
|
||||
|
||||
## Известные ошибки и решения (v0.2.x)
|
||||
|
||||
### mqtt-bridge: MQTT connect timeout / CrashLoopBackOff
|
||||
|
||||
**Симптом:** bridge не подключается к EMQX, логи "MQTT connect timeout"
|
||||
|
||||
**Причина:** EMQX вызывает `/internal/mqtt/auth` при CONNECT. Bridge username `iot-bridge-internal` не содержит `_` -> MQTTAuth парсит namespace_deviceId -> ошибка -> deny -> EMQX возвращает not_authorized -> bridge retry 30с -> выглядит как timeout.
|
||||
|
||||
**Решение (v0.2.3):** В Handler добавлены поля BridgeUsername/BridgePassword. MQTTAuth проверяет bridge credentials ДО парсинга namespace_deviceId. Если username совпадает — allow.
|
||||
|
||||
### Docker image cache на k8s нодах
|
||||
|
||||
**Симптом:** после `docker push` новый образ не используется, pod стартует со старым кодом.
|
||||
|
||||
**Причина:** imagePullPolicy=Always работает, но если тег не изменился Kubernetes может использовать кэш ноды.
|
||||
|
||||
**Решение:** Всегда менять тег при пересборке (v0.2.1 -> v0.2.2 -> v0.2.3 и т.д.)
|
||||
|
||||
### EMQX: required_field node.cookie/node.data_dir
|
||||
|
||||
**Симптом:** EMQX CrashLoopBackOff при первом старте
|
||||
|
||||
**Причина:** EMQX 5.x требует явных node.cookie и node.data_dir
|
||||
|
||||
**Решение:** В emqx.conf (ConfigMap) обязательны:
|
||||
```hocon
|
||||
node { name = "emqx@127.0.0.1", cookie = "...", data_dir = "/opt/emqx/data" }
|
||||
```
|
||||
После изменения ConfigMap: `kubectl rollout restart deployment/emqx -n sless`
|
||||
@@ -1,406 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT MVP — Инженерная документация деплоя
|
||||
|
||||
> Создано: 2026-04-04
|
||||
> Ветка: Ioter
|
||||
> Автор: GitHub Copilot (Claude Sonnet 4.6)
|
||||
|
||||
---
|
||||
|
||||
## Архитектура IoT стека
|
||||
|
||||
```
|
||||
IoT Device (физическое)
|
||||
│ MQTT CONNECT (username="{ns}_{deviceId}", password=hex)
|
||||
▼
|
||||
EMQX 5.5.1 (sless/emqx)
|
||||
│ HTTP POST /internal/mqtt/auth → sless-operator:9090
|
||||
│ (auth backend: проверяет Secret iot-{deviceId} в k8s)
|
||||
▼
|
||||
│ MQTT PUBLISH → topic: "{ns}/telemetry/{deviceId}"
|
||||
▼
|
||||
iot-mqtt-bridge (sless/iot-mqtt-bridge)
|
||||
│ paho.mqtt.golang, подписка на "+/telemetry/+"
|
||||
│ parse topic → namespace из первого сегмента
|
||||
▼
|
||||
RabbitMQ (sless/rabbitmq)
|
||||
│ queue: "iot.{namespace}.telemetry"
|
||||
▼
|
||||
event-dispatcher (sless/event-dispatcher)
|
||||
│ Trigger type=event, queue=iot.{namespace}.telemetry
|
||||
▼
|
||||
Serverless Function (пользовательский handler)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Компоненты
|
||||
|
||||
### 1. CRD IoTDevice
|
||||
|
||||
**Расположение:** `iot/api/v1alpha1/device_types.go`
|
||||
**API group:** `iot.kube5s.ru/v1alpha1`
|
||||
**Манифест:** `iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml`
|
||||
|
||||
Поля Spec:
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|--------------|----------|
|
||||
| `deviceId` | string | да | Идентификатор устройства. Pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$` |
|
||||
| `enabled` | bool | нет | Активно ли устройство (default: true) |
|
||||
| `metadata` | map[string]string | нет | Произвольные метаданные (модель, локация) |
|
||||
|
||||
Поля Status:
|
||||
| Поле | Описание |
|
||||
|------|----------|
|
||||
| `phase` | `Active` / `Disabled` / `Pending` / `Error` |
|
||||
| `mqttUsername` | `{namespace}_{deviceId}` |
|
||||
| `secretName` | Имя k8s Secret с credentials |
|
||||
| `topicPrefix` | `{namespace}/` |
|
||||
| `message` | Сообщение об ошибке если phase=Error |
|
||||
|
||||
### 2. IoT Controller
|
||||
|
||||
**Файл:** `iot/controllers/iotdevice_controller.go`
|
||||
**Логика Reconcile:**
|
||||
|
||||
```
|
||||
IoTDevice CREATE/UPDATE
|
||||
1. Добавить finalizer "iot.kube5s.ru/device-cleanup"
|
||||
2. Если Secret iot-{deviceId} не существует:
|
||||
- Сгенерировать пароль: crypto/rand 32 bytes → hex (64 символа)
|
||||
- OwnerReference → Secret удаляется каскадно при удалении IoTDevice
|
||||
- Secret keys: mqtt-username, mqtt-password, device-id
|
||||
3. Обновить Status: phase=Active, mqttUsername, secretName, topicPrefix
|
||||
4. Если enabled=false → phase=Disabled
|
||||
|
||||
IoTDevice DELETE
|
||||
1. Проверить finalizer
|
||||
2. Secret удаляется каскадно (OwnerReference)
|
||||
3. Убрать finalizer → k8s завершает удаление
|
||||
```
|
||||
|
||||
### 3. IoT REST API
|
||||
|
||||
**Файл:** `internal/api/handler/iot_device_handler.go`
|
||||
|
||||
| Endpoint | Auth | Описание |
|
||||
|----------|------|----------|
|
||||
| `POST /internal/mqtt/auth` | Нет (internal) | MQTT auth backend для EMQX |
|
||||
| `POST /v1/namespaces/{ns}/iot/devices` | JWT | Создать IoTDevice |
|
||||
| `GET /v1/namespaces/{ns}/iot/devices` | JWT | Список (без паролей) |
|
||||
| `GET /v1/namespaces/{ns}/iot/devices/{name}` | JWT | Получить (включая mqtt_password из Secret) |
|
||||
| `DELETE /v1/namespaces/{ns}/iot/devices/{name}` | JWT | Удалить |
|
||||
| `PATCH /v1/namespaces/{ns}/iot/devices/{name}` | JWT | Обновить enabled |
|
||||
|
||||
**MQTT Auth endpoint:**
|
||||
- Всегда HTTP 200 (EMQX игнорирует non-200)
|
||||
- Парсит `username` → `{namespace}_{deviceId}` (разделитель первый `_`)
|
||||
- Ищет k8s Secret `iot-{deviceId}` в namespace
|
||||
- `crypto/subtle.ConstantTimeCompare` для защиты от timing attack
|
||||
|
||||
### 4. EMQX 5.5.1
|
||||
|
||||
**Манифест:** `deployments/k8s/emqx.yaml`
|
||||
**Конфиг:** HOCON `emqx.conf`, монтируется как ConfigMap volume
|
||||
|
||||
**Критически важные поля (без них EMQX 5.x не стартует):**
|
||||
```hocon
|
||||
node {
|
||||
name = "emqx@127.0.0.1" # Обязательно для single-node
|
||||
cookie = "..." # Erlang cluster cookie (любая строка для single-node)
|
||||
data_dir = "/opt/emqx/data" # Директория данных Mnesia
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ EMQX 5.x: поля `node.cookie` и `node.data_dir` — **обязательные** (mandatory),
|
||||
> в отличие от 4.x где были значения по умолчанию.
|
||||
> При обновлении ConfigMap нужен `kubectl rollout restart` — Deployment не перезапускается автоматически.
|
||||
|
||||
**Auth backend:**
|
||||
```hocon
|
||||
authentication = [{
|
||||
mechanism = password_based
|
||||
backend = http
|
||||
method = post
|
||||
url = "http://sless-operator.sless.svc:9090/internal/mqtt/auth"
|
||||
}]
|
||||
```
|
||||
|
||||
### 5. iot-mqtt-bridge
|
||||
|
||||
**Код:** `iot/cmd/mqtt-bridge/main.go`
|
||||
**Манифест:** `deployments/k8s/iot-mqtt-bridge.yaml`
|
||||
**Образ:** тот же что и оператор (`sless-operator:v0.1.50`), бинарь `/iot-mqtt-bridge`
|
||||
|
||||
**Логика:**
|
||||
1. Подключиться к EMQX как MQTT клиент (credentials из Secret `iot-bridge-credentials`)
|
||||
2. Подписаться на `+/telemetry/+` (все namespace, все устройства)
|
||||
3. При получении: извлечь namespace из topic[0], publish в RabbitMQ `iot.{namespace}.telemetry`
|
||||
4. Reconnect loop при обрыве соединения
|
||||
|
||||
**Envelope в RabbitMQ:**
|
||||
```json
|
||||
{
|
||||
"namespace": "sless-user123",
|
||||
"device_id": "sensor-01",
|
||||
"topic": "sless-user123/telemetry/sensor-01",
|
||||
"payload": "<base64 of raw MQTT payload>",
|
||||
"received_at": "2026-04-04T07:19:30Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Terraform Provider
|
||||
|
||||
**Файл:** `terraform/provider/internal/resources/iot_device_resource.go`
|
||||
**Ресурс:** `sless_iot_device`
|
||||
**Версия провайдера:** `0.1.2`
|
||||
|
||||
```hcl
|
||||
resource "sless_iot_device" "temperature_sensor" {
|
||||
name = "temp-sensor-01"
|
||||
device_id = "temp-sensor-01"
|
||||
enabled = true
|
||||
metadata = {
|
||||
model = "DHT22"
|
||||
location = "Warehouse A"
|
||||
}
|
||||
}
|
||||
|
||||
output "mqtt_password" {
|
||||
value = sless_iot_device.temperature_sensor.mqtt_password
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Процедура первого деплоя
|
||||
|
||||
### Предварительные условия
|
||||
- Кластер с namespace `sless`
|
||||
- sless-operator запущен (или будет запущен в шаге 3)
|
||||
- RabbitMQ доступен в кластере
|
||||
|
||||
### Шаги
|
||||
|
||||
**1. Применить CRD (один раз, cluster-wide)**
|
||||
```bash
|
||||
kubectl apply -f iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml
|
||||
```
|
||||
|
||||
**2. Обновить RBAC (добавить права на iot.kube5s.ru)**
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/rbac.yaml
|
||||
```
|
||||
|
||||
**3. Применить EMQX**
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/emqx.yaml
|
||||
kubectl rollout status deployment/emqx -n sless
|
||||
```
|
||||
|
||||
**4. Применить оператор (с IoT поддержкой)**
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/operator.yaml
|
||||
kubectl rollout status deployment/sless-operator -n sless
|
||||
```
|
||||
|
||||
**5. Bootstrap credentials для mqtt-bridge**
|
||||
|
||||
Создать системное IoTDevice устройство для bridge:
|
||||
```bash
|
||||
TOKEN=$(kubectl get secret sless-operator-secret -n sless \
|
||||
-o jsonpath="{.data.SLESS_API_TOKEN}" | base64 -d)
|
||||
|
||||
# Создать IoTDevice
|
||||
curl -X POST https://sless.kube5s.ru/v1/namespaces/sless/iot/devices \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"iot-bridge","device_id":"iot-bridge","enabled":true}'
|
||||
|
||||
# Подождать 5с пока контроллер создаст Secret
|
||||
sleep 5
|
||||
|
||||
# Получить credentials
|
||||
CREDS=$(curl -s https://sless.kube5s.ru/v1/namespaces/sless/iot/devices/iot-bridge \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
MQTT_USER=$(echo $CREDS | jq -r .mqtt_username)
|
||||
MQTT_PASS=$(echo $CREDS | jq -r .mqtt_password)
|
||||
|
||||
# Создать Secret для bridge Deployment
|
||||
kubectl create secret generic iot-bridge-credentials -n sless \
|
||||
--from-literal=MQTT_USERNAME="$MQTT_USER" \
|
||||
--from-literal=MQTT_PASSWORD="$MQTT_PASS"
|
||||
```
|
||||
|
||||
**6. Применить mqtt-bridge**
|
||||
```bash
|
||||
kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml
|
||||
kubectl rollout status deployment/iot-mqtt-bridge -n sless
|
||||
```
|
||||
|
||||
### Ожидаемый результат
|
||||
```
|
||||
emqx-xxx 1/1 Running
|
||||
iot-mqtt-bridge-xxx 1/1 Running
|
||||
sless-operator-xxx 1/1 Running
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Известные ошибки и решения
|
||||
|
||||
### EMQX CrashLoopBackOff: required_field node.cookie/node.data_dir
|
||||
|
||||
**Симптом:** `escript: exception throw: {emqx_conf_schema, [{kind=>validation_error, path=>"node.cookie", reason=>required_field}]}`
|
||||
|
||||
**Причина:** EMQX 5.x требует явного задания `node { cookie, data_dir }` в конфиге.
|
||||
|
||||
**Решение:** Добавить в `emqx.conf`:
|
||||
```hocon
|
||||
node {
|
||||
name = "emqx@127.0.0.1"
|
||||
cookie = "your-cookie-string"
|
||||
data_dir = "/opt/emqx/data"
|
||||
}
|
||||
```
|
||||
После `kubectl apply` — сделать `kubectl rollout restart deployment/emqx -n sless`.
|
||||
|
||||
---
|
||||
|
||||
### RBAC forbidden: iotdevices.iot.kube5s.ru
|
||||
|
||||
**Симптом:** `{"error":"iotdevices.iot.kube5s.ru is forbidden: User \"system:serviceaccount:sless:sless-operator\" cannot create resource"}`
|
||||
|
||||
**Причина:** ClusterRole `sless-operator` не включает API group `iot.kube5s.ru`.
|
||||
|
||||
**Решение:** Добавить в `deployments/k8s/rbac.yaml` и применить:
|
||||
```yaml
|
||||
- apiGroups: ["iot.kube5s.ru"]
|
||||
resources: ["iotdevices"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["iot.kube5s.ru"]
|
||||
resources: ["iotdevices/status"]
|
||||
verbs: ["get", "update", "patch"]
|
||||
- apiGroups: ["iot.kube5s.ru"]
|
||||
resources: ["iotdevices/finalizers"]
|
||||
verbs: ["update"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### mqtt-bridge: multiple restarts при старте
|
||||
|
||||
**Симптом:** `iot-mqtt-bridge RESTARTS=3`
|
||||
|
||||
**Причина:** bridge пытается подключиться к EMQX который ещё не готов. Нормальное поведение.
|
||||
|
||||
**Решение:** Bridge имеет reconnect loop — после старта EMQX подключение восстанавливается автоматически. Ничего делать не нужно.
|
||||
|
||||
---
|
||||
|
||||
## Версии образов
|
||||
|
||||
| Версия | Дата | Изменения |
|
||||
|--------|------|-----------|
|
||||
| v0.1.50 | 2026-04-04 | IoT controller + IoT API + iot-mqtt-bridge бинарь |
|
||||
| v0.1.49 | ранее | До IoT |
|
||||
|
||||
---
|
||||
|
||||
## Актуальный деплой (2026-04-12)
|
||||
|
||||
> Кластер: iot-naeel (новый). Kafka удалён. Postgres — managed. SQS вместо Kafka.
|
||||
|
||||
### Кластер iot-naeel
|
||||
|
||||
- API: `https://185.247.187.149:6443`
|
||||
- Ingress IP: `185.247.187.151`
|
||||
- DNS: `iot.kube5s.ru → 185.247.187.147`
|
||||
- kubeconfig: `~/.kube/config` на ВМ (токен с TTL 24ч, обновляется через auth.k8s.ngcloud.ru)
|
||||
|
||||
### Namespace sless — содержимое
|
||||
|
||||
| Ресурс | Имя | Описание |
|
||||
|--------|-----|----------|
|
||||
| Deployment | emqx | MQTT-брокер EMQX 5.5.1 |
|
||||
| Deployment | iot-operator | Controller-manager + REST API v0.2.0 |
|
||||
| Deployment | iot-mqtt-bridge | MQTT → shared-SQS bridge v0.2.0 |
|
||||
| Deployment | iot-sqs-consumer | shared-SQS → Postgres consumer v0.2.0 |
|
||||
| Service | emqx | :1883 (MQTT), :8083 (WS), :18083 (dashboard) |
|
||||
| Service | emqx-ws | :8083 (для Ingress) |
|
||||
| Service | iot-operator | :9090 (REST API) |
|
||||
| Ingress | emqx-mqtt-websocket | iot.kube5s.ru → /mqtt (WS), /console (UI) |
|
||||
| Secret | iot-sqs-credentials | SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY |
|
||||
| Secret | iot-postgres-secret | POSTGRES_USER, POSTGRES_PASSWORD, IOT_PG_DSN |
|
||||
| Secret | iot-bridge-credentials | MQTT_USERNAME, MQTT_PASSWORD |
|
||||
| CRD | iotdevices.iot.kube5s.ru | IoTDevice v1alpha1 |
|
||||
| ServiceAccount | iot-operator | + ClusterRole + ClusterRoleBinding |
|
||||
|
||||
### Процедура деплоя (актуальная)
|
||||
|
||||
```bash
|
||||
# 0. SSH на ВМ (все команды оттуда)
|
||||
ssh -i ~/.ssh/id_ed25519 naeel@5.172.178.213
|
||||
|
||||
# 1. Сборка и push образа
|
||||
cd /home/naeel/terra/IoT
|
||||
docker build -t naeel/iot-operator:v0.2.0 -t naeel/iot-operator:latest .
|
||||
docker push naeel/iot-operator:v0.2.0
|
||||
docker push naeel/iot-operator:latest
|
||||
|
||||
# 2. CRD (один раз)
|
||||
kubectl apply -f config/crd/bases/iot.kube5s.ru_iotdevices.yaml
|
||||
|
||||
# 3. Postgres Secret (содержит DSN managed PG)
|
||||
kubectl apply -f deployments/k8s/iot-postgres.yaml
|
||||
|
||||
# 4. EMQX
|
||||
kubectl apply -f deployments/k8s/emqx.yaml
|
||||
kubectl apply -f deployments/k8s/emqx-ws-ingress.yaml
|
||||
|
||||
# 5. Operator
|
||||
kubectl apply -f deployments/k8s/iot-operator.yaml
|
||||
|
||||
# 6. SQS credentials (уже создан через kubectl create secret)
|
||||
# kubectl get secret iot-sqs-credentials -n sless
|
||||
|
||||
# 7. MQTT bridge credentials (уже создан)
|
||||
# kubectl get secret iot-bridge-credentials -n sless
|
||||
|
||||
# 8. Bridge + Consumer
|
||||
kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml
|
||||
kubectl apply -f deployments/k8s/iot-sqs-consumer.yaml
|
||||
|
||||
# 9. Проверка
|
||||
kubectl get pods -n sless
|
||||
kubectl logs -n sless deployment/iot-operator --tail=10
|
||||
kubectl logs -n sless deployment/iot-mqtt-bridge --tail=10
|
||||
kubectl logs -n sless deployment/iot-sqs-consumer --tail=10
|
||||
```
|
||||
|
||||
### Managed PostgreSQL
|
||||
|
||||
- Namespace: `dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5`
|
||||
- Pod: `postgresqlk8s-0`
|
||||
- Host: `postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local`
|
||||
- User: `super`, DB: `sqsdb`, PG 17
|
||||
- Тот же инстанс что использует shared-SQS для billing
|
||||
- Credentials: см. `/home/naeel/terra/SQS-service/secrets/iot_pg.md`
|
||||
|
||||
### shared-SQS (очередь вместо Kafka)
|
||||
|
||||
- Namespace: `shared-sqs`
|
||||
- Endpoint: `https://qu.kube5s.ru`
|
||||
- Tenant ID: `t-96afe7e9f781f6ca` (iot-service)
|
||||
- Queue: `iot-telemetry`
|
||||
- Access Key: хранится в Secret `iot-sqs-credentials` namespace `sless`
|
||||
- Admin token: хранится в Secret `shared-sqs-admin` namespace `shared-sqs`
|
||||
|
||||
### Версии образов (актуальные)
|
||||
|
||||
| Версия | Дата | Registry | Изменения |
|
||||
|--------|------|----------|-----------|
|
||||
| v0.2.0 | 2026-04-12 | Docker Hub naeel/iot-operator | Kafka→SQS, managed PG, новый кластер |
|
||||
| v0.1.50 | 2026-04-04 | pearlharbor (Harbor) | IoT controller + API + mqtt-bridge (Kafka) |
|
||||
-1402
File diff suppressed because it is too large
Load Diff
-141
@@ -1,141 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT Managed Service — Трекер прогресса
|
||||
|
||||
## 2026-04-12: Перенос из sless в отдельную репу
|
||||
|
||||
### Выполнено
|
||||
- [x] Создана репа https://gitea.services.ngcloud.ru/Nail/IoT
|
||||
- [x] Go модуль: `gitea.services.ngcloud.ru/Nail/IoT`
|
||||
- [x] Перенесены все IoT-компоненты из sless:
|
||||
- `iot/api/v1alpha1/` → `api/v1alpha1/` (CRD types: IoTDevice)
|
||||
- `iot/controllers/` → `controllers/` (IoTDevice reconciler)
|
||||
- `iot/cmd/mqtt-bridge/` → `cmd/mqtt-bridge/` (MQTT→Kafka)
|
||||
- `iot/cmd/kafka-consumer/` → `cmd/kafka-consumer/` (Kafka→Postgres)
|
||||
- `internal/storage/iotpg/` → `internal/storage/iotpg/` (per-tenant PG)
|
||||
- `internal/api/handler/iot_*.go` → `internal/api/handler/` (REST handlers)
|
||||
- `internal/api/ui/iot-*.html` → `internal/api/ui/` (embedded HTML)
|
||||
- `internal/api/middleware/` → `internal/api/middleware/` (auth, logging)
|
||||
- `deployments/k8s/iot-*.yaml` + `kafka.yaml` → `deployments/k8s/`
|
||||
- `config/crd/bases/` → `config/crd/bases/`
|
||||
- `doc/iot/`, `examples/IOT/` → `doc/`, `examples/`
|
||||
- [x] Созданы новые файлы:
|
||||
- `cmd/iot-operator/main.go` — точка входа (controller-manager + REST API)
|
||||
- `internal/api/handler/handler.go` — упрощённый Handler (без S3/PG от sless)
|
||||
- `internal/api/router.go` — только IoT маршруты
|
||||
- `Dockerfile` — multi-stage build, 3 бинарника, distroless
|
||||
- `Makefile` — build/docker/deploy команды
|
||||
- `.gitignore`
|
||||
- `.github/copilot-instructions.md`
|
||||
- [x] Все import paths заменены: `sless/iot/...` → `Nail/IoT/...`
|
||||
- [x] Все 3 бинарника собираются без ошибок (go build ./...)
|
||||
- [x] Запушено в Gitea
|
||||
|
||||
### Следующие шаги
|
||||
- [ ] Docker build + push на Docker Hub (`naeel/iot-operator`)
|
||||
- [ ] Обновить k8s manifests для нового образа
|
||||
- [ ] Деплой в кластер
|
||||
- [ ] E2E тест: создание устройства → MQTT → Kafka → Postgres → API
|
||||
- [ ] Убрать IoT-код из sless (опционально, после подтверждения что всё работает)
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-12: Замена Kafka → shared-SQS + деплой в новый кластер
|
||||
|
||||
### Выполнено
|
||||
- [x] Создана ветка `feature/replace-kafka-with-sqs`
|
||||
- [x] Документировано решение: `doc/decisions/2026-04-12-replace-kafka-with-sqs.md`
|
||||
- [x] **mqtt-bridge** (`cmd/mqtt-bridge/main.go`): Kafka Writer → AWS SDK SQS SendMessage
|
||||
- [x] **sqs-consumer** (`cmd/sqs-consumer/main.go`): новый — заменяет kafka-consumer, SQS ReceiveMessage → Postgres
|
||||
- [x] **kafka-consumer** (`cmd/kafka-consumer/`): удалён из кода
|
||||
- [x] **admin stats** (`internal/api/handler/iot_admin_stats_handler.go`): Kafka lag → SQS GetQueueAttributes
|
||||
- [x] **handler.go**: убрано поле KafkaBrokers
|
||||
- [x] **iot-operator/main.go**: убрана передача KAFKA_BROKERS
|
||||
- [x] go.mod: убран `segmentio/kafka-go`, добавлен `aws-sdk-go-v2` (v1.41.5 + sqs v1.42.25)
|
||||
- [x] Dockerfile: kafka-consumer → sqs-consumer
|
||||
- [x] Makefile: build-consumer → sqs-consumer
|
||||
- [x] .gitignore: исправлен баг (паттерны без `/` игнорировали `cmd/` директории)
|
||||
- [x] `go build ./...` проходит на ВМ
|
||||
- [x] Коммит и пуш в ветку
|
||||
|
||||
### Деплой в кластер iot-naeel (новый кластер)
|
||||
- [x] shared-SQS: создан tenant `iot-service` (t-96afe7e9f781f6ca), очередь `iot-telemetry`
|
||||
- [x] Namespace `sless` создан
|
||||
- [x] Secret `iot-sqs-credentials` создан
|
||||
- [x] Secret `iot-bridge-credentials` создан
|
||||
- [x] Postgres: переключён на managed PG17 (тот же что SQS billing)
|
||||
- [x] Secret `iot-postgres-secret` создан (DSN → managed PG)
|
||||
- [x] Docker: собран и запушен `naeel/iot-operator:v0.2.0` в Docker Hub
|
||||
- [x] Deployment YAML: обновлены image на Docker Hub, убраны imagePullSecrets
|
||||
- [x] EMQX: задеплоен (emqx.yaml + emqx-ws-ingress.yaml, адаптирован из sless)
|
||||
- [x] iot-operator: задеплоен (новый iot-operator.yaml с RBAC)
|
||||
- [x] CRD `iotdevices.iot.kube5s.ru` установлен
|
||||
- [x] iot-mqtt-bridge: задеплоен, Running
|
||||
- [x] iot-sqs-consumer: задеплоен, Running — подключился к PG и SQS
|
||||
- [x] cert-manager: выпускает TLS для iot.kube5s.ru
|
||||
- [x] Коммит и пуш
|
||||
|
||||
### Что работает
|
||||
- Все 4 пода Running 1/1
|
||||
- iot-operator: controller стартовал, MQTT auth/acl обрабатывает запросы от EMQX
|
||||
- sqs-consumer: подключился к Postgres и SQS, polling loop запущен
|
||||
- mqtt-bridge: подключён к EMQX и SQS, SQS queue resolved
|
||||
- EMQX: MQTT :1883, WS :8083, dashboard :18083
|
||||
|
||||
### Следующие шаги
|
||||
- [ ] E2E тест: создание устройства → MQTT → SQS → Postgres → API
|
||||
- [ ] Нагрузочный тест
|
||||
- [ ] Мерж ветки в master
|
||||
- [ ] Убрать IoT-код из sless (опционально)
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-12: Документация v0.2.3
|
||||
|
||||
### Выполнено
|
||||
- [x] Аудит документации из репозитория sless (thinking 04-04/05/06/09, decisions, progress, api, examples)
|
||||
- [x] Аудит документации в IoT репозитории (deployment.md, overview.md, endpoints.md, mvp-plan.md)
|
||||
- [x] Анализ расхождений — определены устаревшие секции (Kafka, RabbitMQ)
|
||||
- [x] Решение: старые документы НЕ удалять, новые создавать отдельно
|
||||
- [x] Создан doc/deployment-v0.2.3.md (295 строк) — актуальная инструкция деплоя
|
||||
- [x] Создан doc/architecture/current-v0.2.3.md (256 строк) — полная архитектура v0.2.3
|
||||
- [x] Создан doc/api/endpoints-v0.2.3.md (~300 строк) — полная документация REST API
|
||||
- [x] Создан doc/run-and-test.md (~180 строк) — руководство по запуску и E2E тесту
|
||||
- [x] Обновлён doc/progress.md
|
||||
- [x] Дописан doc/thinking/2026-04-12.md — лог мышления сессии
|
||||
|
||||
### Баг-фиксы v0.2.1—v0.2.3
|
||||
- v0.2.1: mqtt-bridge — bridge auth fix (BridgeUsername/BridgePassword проверяется ДО парсинга username)
|
||||
- v0.2.3: финальный образ со всеми фиксами
|
||||
|
||||
### Документация (сессия 2, вечер)
|
||||
|
||||
Проведён полный аудит IoT-документации из sless репы. Прочитано:
|
||||
- sless/doc/thinking/2026-04-04.md (681 строк) — архитектура, первый деплой
|
||||
- sless/doc/thinking/2026-04-05.md (178 строк) — телеметрия, MQTTX, UX
|
||||
- sless/doc/thinking/2026-04-06.md (613 строк) — Kafka pipeline, 8 тестов
|
||||
- sless/doc/thinking/2026-04-09.md (349 строк) — shared-sqs, TenantStore
|
||||
- sless/doc/iot/deployment.md — инженерный документ деплоя (RabbitMQ)
|
||||
- sless/doc/progress.md — IoT-записи
|
||||
- sless/examples/IOT/ — E2E demo
|
||||
|
||||
Созданы НОВЫЕ актуальные файлы (старые НЕ тронуты — база знаний):
|
||||
- [x] doc/deployment-v0.2.3.md — полная процедура деплоя (SQS, managed PG, bridge auth)
|
||||
- [x] doc/architecture/current-v0.2.3.md — актуальная архитектура (без Kafka/RabbitMQ)
|
||||
- [x] doc/api/endpoints-v0.2.3.md — все API эндпоинты с примерами запросов/ответов
|
||||
- [x] doc/run-and-test.md — как запустить и протестировать E2E
|
||||
|
||||
## 2026-04-12 — v0.2.5: баг-фиксы и E2E тест
|
||||
|
||||
### Найдены и исправлены баги:
|
||||
1. **MQTTAuth: Get по Name=deviceID** — заменено на List + фильтр по Spec.DeviceID
|
||||
2. **PG15+ GRANT**: добавлен `GRANT role TO CURRENT_USER` перед `CREATE DATABASE ... OWNER`
|
||||
|
||||
### E2E тест пройден:
|
||||
- Устройство создано через REST API (201)
|
||||
- MQTT CONNECT + PUBLISH через EMQX — OK
|
||||
- mqtt-bridge → SQS forwarding — OK
|
||||
- sqs-consumer → Postgres (tenant DB created + telemetry saved) — OK
|
||||
- REST API GET /telemetry — 2 записи с payload
|
||||
|
||||
### Docker Hub: `naeel/iot-operator:v0.2.5`
|
||||
@@ -1,251 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT — Руководство по запуску и тестированию (v0.2.3)
|
||||
|
||||
> Дата: 2026-04-12
|
||||
> Кластер: iot-naeel, namespace: sless
|
||||
> Образ: naeel/iot-operator:v0.2.3
|
||||
|
||||
---
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Предварительные требования](#предварительные-требования)
|
||||
2. [Сборка и публикация образа](#сборка-и-публикация-образа)
|
||||
3. [Деплой в кластер](#деплой-в-кластер)
|
||||
4. [E2E тест: полный путь данных](#e2e-тест-полный-путь-данных)
|
||||
5. [Проверка компонентов](#проверка-компонентов)
|
||||
6. [Отладка](#отладка)
|
||||
|
||||
---
|
||||
|
||||
## Предварительные требования
|
||||
|
||||
- kubectl с kubeconfig для кластера iot-naeel (185.247.187.149:6443)
|
||||
- Docker для сборки образа
|
||||
- mosquitto-clients для MQTT тестов (apt install mosquitto-clients)
|
||||
- curl для REST API
|
||||
|
||||
---
|
||||
|
||||
## Сборка и публикация образа
|
||||
|
||||
```bash
|
||||
cd /home/naeel/terra/IoT
|
||||
|
||||
# Сборка multi-binary Docker образа
|
||||
docker build -t naeel/iot-operator:v0.2.3 .
|
||||
|
||||
# Push на Docker Hub
|
||||
docker push naeel/iot-operator:v0.2.3
|
||||
```
|
||||
|
||||
Dockerfile собирает 3 бинарника: /iot-operator, /mqtt-bridge, /sqs-consumer.
|
||||
|
||||
---
|
||||
|
||||
## Деплой в кластер
|
||||
|
||||
```bash
|
||||
# Применить CRD
|
||||
kubectl apply -f config/crd/bases/iot.kube5s.ru_iotdevices.yaml
|
||||
|
||||
# Деплой всех компонентов
|
||||
kubectl apply -f deployments/k8s/emqx.yaml
|
||||
kubectl apply -f deployments/k8s/iot-operator.yaml
|
||||
kubectl apply -f deployments/k8s/iot-mqtt-bridge.yaml
|
||||
kubectl apply -f deployments/k8s/iot-sqs-consumer.yaml
|
||||
kubectl apply -f deployments/k8s/iot-postgres.yaml
|
||||
|
||||
# Проверка
|
||||
kubectl get pods -n sless
|
||||
# Ожидаем: emqx, iot-operator, iot-mqtt-bridge, iot-sqs-consumer — все Running
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E2E тест: полный путь данных
|
||||
|
||||
Полный тест проверяет цепочку: REST API → CRD → Secret → MQTT → SQS → Postgres → Telemetry API.
|
||||
|
||||
### Шаг 1: Создать устройство через API
|
||||
|
||||
```bash
|
||||
curl -X POST https://iot.kube5s.ru/v1/namespaces/sless/iot/devices \
|
||||
-H "Authorization: Bearer test-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"e2e-test-01","device_id":"e2e-test-01","enabled":true}'
|
||||
```
|
||||
|
||||
Ответ: 201 Created с данными устройства.
|
||||
|
||||
### Шаг 2: Получить MQTT credentials
|
||||
|
||||
```bash
|
||||
curl -s https://iot.kube5s.ru/v1/namespaces/sless/iot/devices/e2e-test-01 \
|
||||
-H "Authorization: Bearer test-token" \
|
||||
| jq '{mqtt_username, mqtt_password, phase}'
|
||||
```
|
||||
|
||||
Ожидаем: phase=Active, mqtt_username="sless_e2e-test-01", mqtt_password=hex(64).
|
||||
|
||||
> Если phase=Pending — подождать 1-2 секунды, контроллер ещё создаёт Secret.
|
||||
|
||||
### Шаг 3: Отправить телеметрию через MQTT
|
||||
|
||||
```bash
|
||||
# Вставить реальный пароль из Шага 2
|
||||
MQTT_USER="sless_e2e-test-01"
|
||||
MQTT_PASS="<mqtt_password из шага 2>"
|
||||
|
||||
# Через WebSocket (через Ingress)
|
||||
mosquitto_pub \
|
||||
-h iot.kube5s.ru \
|
||||
-p 443 \
|
||||
--capath /etc/ssl/certs \
|
||||
-u "$MQTT_USER" \
|
||||
-P "$MQTT_PASS" \
|
||||
-t "sless/telemetry/e2e-test-01" \
|
||||
-m '{"temperature": 22.5, "humidity": 65}' \
|
||||
--protocol-version mqttv5
|
||||
|
||||
# ИЛИ через kubectl port-forward (MQTT напрямую)
|
||||
kubectl port-forward svc/emqx -n sless 1883:1883 &
|
||||
mosquitto_pub \
|
||||
-h localhost \
|
||||
-p 1883 \
|
||||
-u "$MQTT_USER" \
|
||||
-P "$MQTT_PASS" \
|
||||
-t "sless/telemetry/e2e-test-01" \
|
||||
-m '{"temperature": 22.5, "humidity": 65}'
|
||||
```
|
||||
|
||||
### Шаг 4: Проверить что телеметрия дошла до Postgres
|
||||
|
||||
Подождать 5-10 секунд (SQS long polling + обработка).
|
||||
|
||||
```bash
|
||||
curl -s "https://iot.kube5s.ru/v1/namespaces/sless/iot/telemetry?device=e2e-test-01&limit=10" \
|
||||
-H "Authorization: Bearer test-token" \
|
||||
| jq '.'
|
||||
```
|
||||
|
||||
Ожидаем: count >= 1, items содержит запись с payload {"temperature": 22.5, "humidity": 65}.
|
||||
|
||||
### Шаг 5: Очистка
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://iot.kube5s.ru/v1/namespaces/sless/iot/devices/e2e-test-01 \
|
||||
-H "Authorization: Bearer test-token"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка компонентов
|
||||
|
||||
### Поды
|
||||
|
||||
```bash
|
||||
kubectl get pods -n sless -l 'app in (iot-operator,iot-mqtt-bridge,iot-sqs-consumer,emqx)'
|
||||
```
|
||||
|
||||
### Логи
|
||||
|
||||
```bash
|
||||
# Operator (controller + API)
|
||||
kubectl logs -n sless deployment/iot-operator --tail=50
|
||||
|
||||
# MQTT Bridge
|
||||
kubectl logs -n sless deployment/iot-mqtt-bridge --tail=50
|
||||
|
||||
# SQS Consumer
|
||||
kubectl logs -n sless deployment/iot-sqs-consumer --tail=50
|
||||
|
||||
# EMQX
|
||||
kubectl logs -n sless deployment/emqx --tail=50
|
||||
```
|
||||
|
||||
### CRD ресурсы
|
||||
|
||||
```bash
|
||||
# Список IoTDevice
|
||||
kubectl get iotdevices -n sless
|
||||
|
||||
# Детали
|
||||
kubectl describe iotdevice e2e-test-01 -n sless
|
||||
|
||||
# Секреты
|
||||
kubectl get secret -n sless -l app.kubernetes.io/managed-by=iot-operator
|
||||
```
|
||||
|
||||
### EMQX Dashboard (отладка)
|
||||
|
||||
```bash
|
||||
kubectl port-forward svc/emqx -n sless 18083:18083
|
||||
# Открыть http://localhost:18083
|
||||
# Default: admin / public
|
||||
```
|
||||
|
||||
### SQS очередь
|
||||
|
||||
```bash
|
||||
# Через admin stats endpoint
|
||||
curl -s https://iot.kube5s.ru/iot-admin/stats \
|
||||
-H "Authorization: Bearer <ADMIN_STATS_TOKEN>" \
|
||||
| jq '.sqs'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Отладка
|
||||
|
||||
### Устройство не подключается по MQTT
|
||||
|
||||
1. Проверить phase: `kubectl get iotdevice {name} -n {ns} -o jsonpath='{.status.phase}'`
|
||||
— Должно быть `Active`
|
||||
2. Проверить Secret: `kubectl get secret iot-{deviceId} -n {ns}`
|
||||
3. Проверить EMQX logs: `kubectl logs -n sless deployment/emqx | grep "auth"`
|
||||
4. Ручная проверка auth:
|
||||
```bash
|
||||
kubectl port-forward svc/iot-operator -n sless 9090:9090
|
||||
curl -X POST http://localhost:9090/internal/mqtt/auth \
|
||||
-d '{"username":"sless_sensor-01","password":"...","clientid":"test"}'
|
||||
```
|
||||
|
||||
### Телеметрия не появляется в API
|
||||
|
||||
1. Bridge подключён? `kubectl logs -n sless deployment/iot-mqtt-bridge | tail -20`
|
||||
2. SQS получает? `curl /iot-admin/stats` → sqs.approximate_messages
|
||||
3. Consumer обрабатывает? `kubectl logs -n sless deployment/iot-sqs-consumer | tail -20`
|
||||
4. Postgres доступен? Проверить логи consumer на ошибки подключения
|
||||
|
||||
### Bridge не подключается к MQTT
|
||||
|
||||
1. Проверить Secret iot-bridge-credentials:
|
||||
`kubectl get secret iot-bridge-credentials -n sless -o jsonpath='{.data.MQTT_USERNAME}' | base64 -d`
|
||||
2. Проверить env в Bridge pod:
|
||||
`kubectl exec -n sless deployment/iot-mqtt-bridge -- env | grep MQTT`
|
||||
|
||||
---
|
||||
|
||||
## IoT Console (Web UI)
|
||||
|
||||
Открыть `https://iot.kube5s.ru/console` в браузере.
|
||||
|
||||
Функции:
|
||||
- CRUD устройств через REST API
|
||||
- Встроенный MQTT WebSocket клиент (wss://iot.kube5s.ru/mqtt)
|
||||
- Real-time отображение входящей телеметрии
|
||||
- Просмотр истории из Postgres
|
||||
|
||||
---
|
||||
|
||||
## IoT Admin Panel
|
||||
|
||||
Открыть `https://iot.kube5s.ru/iot-admin` в браузере.
|
||||
|
||||
Функции:
|
||||
- Статистика по PostgreSQL (количество записей по тенантам)
|
||||
- SQS: approximate message count
|
||||
- Статус подов bridge и consumer
|
||||
- Автообновление каждые 30 секунд
|
||||
@@ -1,537 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# IoT — Отчёт по тестированию v0.2.5 / v0.2.6
|
||||
|
||||
> Дата: 2026-04-21
|
||||
> Кластер: iot-naeel, namespace: sless
|
||||
> Образ: naeel/iot-operator:v0.2.6
|
||||
> Тестировал: GitHub Copilot (Claude Sonnet 4.6)
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
В ходе двух сессий (2026-04-12 и 2026-04-21) после деплоя v0.2.3 были обнаружены и
|
||||
исправлены три бага, выпущены v0.2.4 → v0.2.5 → v0.2.6. Затем проведено расширенное
|
||||
тестирование: 7 категорий, 33 теста.
|
||||
|
||||
---
|
||||
|
||||
## Баги, найденные в процессе тестирования
|
||||
|
||||
### BUG-01 — MQTTAuth ищет IoTDevice по Name=deviceID (критический)
|
||||
|
||||
**Версия:** v0.2.3
|
||||
**Компонент:** `internal/api/handler/iot_device_handler.go`, функция `MQTTAuth`
|
||||
**Симптом:** MQTT CONNECT возвращал `Connection Refused: not authorised (5)` для всех устройств.
|
||||
**Причина:** Хэндлер вызывал `h.K8s.Get(client.ObjectKey{Name: deviceID})`, где `deviceID` — это
|
||||
`Spec.DeviceID` (`e2e-test-01`), но имя K8s объекта IoTDevice может быть другим (`e2e-test-device`).
|
||||
Объект не находился → `NotFound` → `deny`.
|
||||
**Диагностика:**
|
||||
```bash
|
||||
# Auth endpoint напрямую возвращал deny при правильном пароле:
|
||||
curl -s -X POST http://iot-operator.sless.svc:9090/internal/mqtt/auth \
|
||||
-d username:sless_e2e-test-01
|
||||
# {"result":"deny"}
|
||||
|
||||
# Secret существует и пароль совпадает:
|
||||
kubectl get secret iot-e2e-test-01 -n sless -o jsonpath="{.data.mqtt-password}" | base64 -d
|
||||
# fa8180c07ffc1d665d21e95a443235a93926f16e9d98c319539263f05a333ef0
|
||||
|
||||
# IoTDevice объект называется e2e-test-device, а не e2e-test-01:
|
||||
kubectl get iotdevice -n sless
|
||||
# NAME DEVICEID
|
||||
# e2e-test-device e2e-test-01
|
||||
```
|
||||
**Фикс:** Заменён `Get` на `List` + фильтр по `Spec.DeviceID == deviceID`.
|
||||
**Версия фикса:** v0.2.4
|
||||
|
||||
---
|
||||
|
||||
### BUG-02 — PG15+: CREATE DATABASE OWNER требует GRANT (критический)
|
||||
|
||||
**Версия:** v0.2.4
|
||||
**Компонент:** `internal/storage/iotpg/iot_telemetry_store.go`, функция `EnsureTenantDB`
|
||||
**Симптом:** sqs-consumer не мог создать tenant DB для первого сообщения от нового namespace.
|
||||
```
|
||||
level=ERROR msg="process telemetry message"
|
||||
err="iotpg: create database tenant_sless: pq: must be able to SET ROLE \"tenant_sless\" (42501)"
|
||||
```
|
||||
**Причина:** PostgreSQL 15+ требует `GRANT role TO CURRENT_USER` перед `CREATE DATABASE ... OWNER role`.
|
||||
В PG17 это обязательно.
|
||||
**Фикс:** Добавлен `GRANT {userName} TO CURRENT_USER` перед `CREATE DATABASE`.
|
||||
**Версия фикса:** v0.2.5
|
||||
|
||||
---
|
||||
|
||||
### BUG-03 — Query param `?device=` вместо `?device_id=` (minor)
|
||||
|
||||
**Версия:** v0.2.5
|
||||
**Компонент:** `internal/api/handler/iot_telemetry_handler.go`, функция `ListIoTTelemetry`
|
||||
**Симптом:** `GET /telemetry?device_id=dev-02` возвращал все записи (фильтр не работал).
|
||||
```bash
|
||||
curl ".../telemetry?device_id=dev-02"
|
||||
# {"count":6,...} ← должно быть count:1
|
||||
```
|
||||
**Причина:** Хэндлер читал `r.URL.Query().Get("device")`, а не `"device_id"`.
|
||||
Несоответствие с именем поля в ответе (`device_id`) и документацией.
|
||||
**Фикс:** Параметр переименован в `device_id`.
|
||||
**Версия фикса:** v0.2.6
|
||||
|
||||
---
|
||||
|
||||
## Состояние кластера на момент тестирования
|
||||
|
||||
```
|
||||
Кластер: iot-naeel (185.247.187.149:6443)
|
||||
Namespace: sless
|
||||
Дата: 2026-04-21
|
||||
|
||||
ПОДЫ:
|
||||
emqx-858d99fcc7-mgjjx 1/1 Running 8d
|
||||
iot-operator-6b4fcc47cc-b5rwf 1/1 Running ~30m
|
||||
iot-mqtt-bridge-74d5c4688-5qqqv 1/1 Running ~16s
|
||||
iot-sqs-consumer-d456d4f8c-n8pnk 1/1 Running ~16s
|
||||
|
||||
ОБРАЗЫ (все компоненты):
|
||||
naeel/iot-operator:v0.2.6
|
||||
|
||||
УСТРОЙСТВА:
|
||||
e2e-test-device device_id=e2e-test-01 phase=Active enabled=true
|
||||
e2e-dev-02 device_id=dev-02 phase=Active enabled=true
|
||||
e2e-dev-03 device_id=dev-03 phase=Active enabled=true
|
||||
e2e-dev-04 device_id=dev-04 phase=Active enabled=true
|
||||
|
||||
ТЕЛЕМЕТРИЯ В POSTGRES (tenant_sless):
|
||||
e2e-test-01: >1000 записей (cap API = 1000)
|
||||
dev-02: 21 запись
|
||||
dev-03: 21 запись
|
||||
dev-04: 21 запись
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Результаты тестирования
|
||||
|
||||
### Группа 1 — E2E пайплайн (базовый)
|
||||
|
||||
Тесты из сессии 2026-04-12, подтверждены после деплоя v0.2.5.
|
||||
|
||||
| # | Тест | Команда / Действие | Ожидание | Результат |
|
||||
|---|------|--------------------|----------|-----------|
|
||||
| 1.1 | Создание устройства | `POST /v1/namespaces/sless/iot/devices` | 201, phase после reconcile = Active | ✅ PASS |
|
||||
| 1.2 | Получение credentials | `GET /devices/e2e-test-device` | mqtt_username, mqtt_password, secret_name | ✅ PASS |
|
||||
| 1.3 | MQTT CONNECT | `mosquitto_pub -u sless_e2e-test-01 -P <pass>` | CONNACK(0) | ✅ PASS |
|
||||
| 1.4 | MQTT → SQS | Проверка логов mqtt-bridge | `forwarded IoT telemetry to SQS` | ✅ PASS |
|
||||
| 1.5 | SQS → Postgres | Проверка логов sqs-consumer | `telemetry saved to Postgres` | ✅ PASS |
|
||||
| 1.6 | Telemetry API | `GET /telemetry?device_id=e2e-test-01` | count≥1, payload совпадает | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 2 — Device API: коды ошибок
|
||||
|
||||
| # | Тест | Запрос | Ожидание | Факт | Результат |
|
||||
|---|------|--------|----------|------|-----------|
|
||||
| 2.1 | POST без `name` | `{}` | 400 `name is required` | 400 ✓ | ✅ PASS |
|
||||
| 2.2 | POST без `device_id` | `{"name":"x"}` | 400 `device_id is required` | 400 ✓ | ✅ PASS |
|
||||
| 2.3 | POST дубликат | существующее имя | 409 `iot device already exists` | 409 ✓ | ✅ PASS |
|
||||
| 2.4 | GET несуществующий | `GET /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS |
|
||||
| 2.5 | DELETE несуществующий | `DELETE /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS |
|
||||
| 2.6 | PATCH без `enabled` | `{}` | 400 `enabled field is required` | 400 ✓ | ✅ PASS |
|
||||
| 2.7 | POST невалидный JSON | `not-json` | 400 `invalid JSON: ...` | 400 ✓ | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 3 — MQTT Auth: edge cases
|
||||
|
||||
Все запросы идут на `POST /internal/mqtt/auth`. Корректный ответ всегда HTTP 200.
|
||||
Результат определяется полем `result` в теле: `"allow"` или `"deny"`.
|
||||
|
||||
| # | Тест | Входные данные | Ожидание | Результат |
|
||||
|---|------|----------------|----------|-----------|
|
||||
| 3.1 | Неверный пароль | правильный username, неверный pass | deny | ✅ PASS |
|
||||
| 3.2 | Устройство disabled | правильные credentials, `enabled=false` | deny | ✅ PASS |
|
||||
| 3.3 | Пустое тело | `{}` | deny | ✅ PASS |
|
||||
| 3.4 | Username без `_` | `"username":"nounderscore"` | deny | ✅ PASS |
|
||||
| 3.5 | Несуществующий deviceID | `"username":"sless_ghost-999"` | deny | ✅ PASS |
|
||||
| 3.6 | Пустой namespace | `"username":"_dev01"` | deny | ✅ PASS |
|
||||
| 3.7 | Пустой deviceID | `"username":"sless_"` | deny | ✅ PASS |
|
||||
| 3.8 | Bridge: неверный пароль | bridge username, wrong pass | deny | ✅ PASS |
|
||||
| 3.9 | Невалидный JSON body | `not-json` | deny | ✅ PASS |
|
||||
| 3.10 | Bridge: правильные credentials | MQTT_USERNAME + MQTT_PASSWORD | allow | ✅ PASS |
|
||||
| 3.11 | Правильные device credentials | sless_e2e-test-01 + корректный pass | allow + ACL rules | ✅ PASS |
|
||||
|
||||
**ACL в ответе при allow (пример для устройства):**
|
||||
```json
|
||||
{
|
||||
"result": "allow",
|
||||
"acl": [
|
||||
{"permission":"allow","action":"publish","topic":"sless/telemetry/e2e-test-01"},
|
||||
{"permission":"allow","action":"subscribe","topic":"sless/telemetry/e2e-test-01"},
|
||||
{"permission":"deny","action":"all","topic":"#"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Группа 4 — Device lifecycle (PATCH / DELETE)
|
||||
|
||||
| # | Тест | Действие | Ожидание | Результат |
|
||||
|---|------|----------|----------|-----------|
|
||||
| 4.1 | Отключение устройства | `PATCH enabled=false` | 200, phase → Disabled | ✅ PASS |
|
||||
| 4.2 | Auth отключённого | auth с правильным паролем | deny | ✅ PASS |
|
||||
| 4.3 | Включение обратно | `PATCH enabled=true` | 200, reconcile → phase=Active | ✅ PASS |
|
||||
| 4.4 | DELETE устройства | `DELETE /devices/to-delete` | 204 | ✅ PASS |
|
||||
| 4.5 | Cascade: Secret удалён | `kubectl get secret iot-del-01` | NotFound | ✅ PASS |
|
||||
| 4.6 | Cascade: IoTDevice удалён | `kubectl get iotdevice to-delete` | NotFound | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 5 — ACL изоляция топиков
|
||||
|
||||
| # | Тест | Действие | Ожидание | Результат |
|
||||
|---|------|----------|----------|-----------|
|
||||
| 5.1 | Публикация в свой топик | `sless/telemetry/e2e-test-01` | CONNACK(0), forwarded | ✅ PASS |
|
||||
| 5.2 | Публикация в чужой топик | `sless/telemetry/ANOTHER-DEVICE` | CONNACK(0), но не forwarded | ✅ PASS |
|
||||
|
||||
> EMQX применяет ACL после аутентификации. При публикации в запрещённый топик
|
||||
> клиент получает CONNACK(0) (аутентификация прошла), но PUBLISH тихо отбрасывается.
|
||||
> Bridge не получает сообщение — подтверждено отсутствием записи в логах.
|
||||
|
||||
---
|
||||
|
||||
### Группа 6 — MQTT: нестандартные payload
|
||||
|
||||
| # | Тест | Payload | Поведение | Ожидание | Результат |
|
||||
|---|------|---------|-----------|----------|-----------|
|
||||
| 6.1 | Невалидный JSON | `THIS IS NOT JSON AT ALL !@#` | bridge оборачивает в строку | `"payload":"THIS IS NOT JSON AT ALL !@#"` | ✅ PASS (by design) |
|
||||
| 6.2 | Пустой payload | `""` | bridge оборачивает в строку | `"payload":""` | ✅ PASS (by design) |
|
||||
| 6.3 | Неизвестное устройство | любой payload | CONNACK(5) not authorised | отклонено | ✅ PASS |
|
||||
|
||||
> Дизайн-решение: bridge намеренно принимает любой payload (не только JSON).
|
||||
> Невалидный payload оборачивается в JSON-строку (`json.Marshal(string(payload))`).
|
||||
> Это позволяет передавать raw данные от устройств старых форматов.
|
||||
|
||||
---
|
||||
|
||||
### Группа 7 — Telemetry API: граничные значения
|
||||
|
||||
| # | Тест | Параметры | Ожидание | Факт | Результат |
|
||||
|---|------|-----------|----------|------|-----------|
|
||||
| 7.1 | limit=2 | `?limit=2` | count=2 | count=2 ✓ | ✅ PASS |
|
||||
| 7.2 | limit=0 (default) | `?limit=0` | count=50 | count=50 ✓ | ✅ PASS |
|
||||
| 7.3 | limit отрицательный | `?limit=-1` | count=50 (default) | count=50 ✓ | ✅ PASS |
|
||||
| 7.4 | limit cap | `?limit=9999`, >1000 записей | count=1000 | count=1000 ✓ | ✅ PASS |
|
||||
| 7.5 | Фильтр device_id | `?device_id=dev-02` | только записи dev-02 | count=21, все dev-02 ✓ | ✅ PASS |
|
||||
| 7.6 | Несуществующий device_id | `?device_id=ghost` | count=0, items=[] | count=0 ✓ | ✅ PASS |
|
||||
| 7.7 | Несуществующий namespace | `/namespaces/unknown-ns-xyz/...` | count=0, items=[] (нет tenant DB) | count=0 ✓ | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 8 — Нагрузочные тесты
|
||||
|
||||
| # | Тест | Параметры | Ожидание | Результат |
|
||||
|---|------|-----------|----------|-----------|
|
||||
| 8.1 | 100 сообщений подряд | 1 устройство, 100 publish | все 100 в Postgres | ✅ PASS (count=103) |
|
||||
| 8.2 | 900 сообщений подряд | 1 устройство, 900 publish | все в Postgres | ✅ PASS |
|
||||
| 8.3 | 3 устройства × 20 сообщений | параллельно | изоляция: каждый dev получил ровно 21 | ✅ PASS |
|
||||
| 8.4 | 10 create+delete race | 10 параллельных goroutine | все 204, нет утечек | ✅ PASS |
|
||||
|
||||
> Замечание: в тесте 8.1 и 8.2 использовался один и тот же device.
|
||||
> Финальный count dev-02/03/04 = 21 (1 из предыдущей сессии + 20 нагрузочных).
|
||||
|
||||
---
|
||||
|
||||
## Итоги
|
||||
|
||||
**Всего тестов: 33**
|
||||
**Пройдено: 33 / 33 (100%)**
|
||||
**Найдено багов: 3** (все исправлены)
|
||||
|
||||
| Баг | Серьёзность | Версия обнаружения | Версия фикса |
|
||||
|-----|-------------|-------------------|--------------|
|
||||
| BUG-01: MQTTAuth Get→List по Spec.DeviceID | Critical | v0.2.3 | v0.2.4 |
|
||||
| BUG-02: PG15+ GRANT перед CREATE DATABASE OWNER | Critical | v0.2.4 | v0.2.5 |
|
||||
| BUG-03: query param `device` → `device_id` | Minor | v0.2.5 | v0.2.6 |
|
||||
|
||||
**Финальная версия: `naeel/iot-operator:v0.2.6`**
|
||||
|
||||
---
|
||||
|
||||
## История версий
|
||||
|
||||
| Версия | Дата | Изменение |
|
||||
|--------|------|-----------|
|
||||
| v0.2.3 | 2026-04-12 | Деплой: bridge auth fix (bridge username без `_`) |
|
||||
| v0.2.4 | 2026-04-12 | Fix BUG-01: MQTTAuth List вместо Get |
|
||||
| v0.2.5 | 2026-04-12 | Fix BUG-02: GRANT перед CREATE DATABASE (PG15+) |
|
||||
| v0.2.6 | 2026-04-21 | Fix BUG-03: query param device→device_id |
|
||||
EOF cat > /home/naeel/terra/IoT/doc/testing-v0.2.6.md << 'EOF'
|
||||
# IoT — Отчёт по тестированию v0.2.5 / v0.2.6
|
||||
|
||||
> Дата: 2026-04-21
|
||||
> Кластер: iot-naeel, namespace: sless
|
||||
> Образ: naeel/iot-operator:v0.2.6
|
||||
> Тестировал: GitHub Copilot (Claude Sonnet 4.6)
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
В ходе двух сессий (2026-04-12 и 2026-04-21) после деплоя v0.2.3 были обнаружены и
|
||||
исправлены три бага, выпущены v0.2.4 → v0.2.5 → v0.2.6. Затем проведено расширенное
|
||||
тестирование: 7 категорий, 33 теста.
|
||||
|
||||
---
|
||||
|
||||
## Баги, найденные в процессе тестирования
|
||||
|
||||
### BUG-01 — MQTTAuth ищет IoTDevice по Name=deviceID (критический)
|
||||
|
||||
**Версия:** v0.2.3
|
||||
**Компонент:** `internal/api/handler/iot_device_handler.go`, функция `MQTTAuth`
|
||||
**Симптом:** MQTT CONNECT возвращал `Connection Refused: not authorised (5)` для всех устройств.
|
||||
**Причина:** Хэндлер вызывал `h.K8s.Get(client.ObjectKey{Name: deviceID})`, где `deviceID` — это
|
||||
`Spec.DeviceID` (`e2e-test-01`), но имя K8s объекта IoTDevice может быть другим (`e2e-test-device`).
|
||||
Объект не находился → `NotFound` → `deny`.
|
||||
**Диагностика:**
|
||||
```bash
|
||||
# Auth endpoint напрямую возвращал deny при правильном пароле:
|
||||
curl -s -X POST http://iot-operator.sless.svc:9090/internal/mqtt/auth \
|
||||
-d password:<correct-pass>
|
||||
# {"result":"deny"}
|
||||
|
||||
# Secret существует и пароль совпадает:
|
||||
kubectl get secret iot-e2e-test-01 -n sless -o jsonpath="{.data.mqtt-password}" | base64 -d
|
||||
# fa8180c07ffc1d665d21e95a443235a93926f16e9d98c319539263f05a333ef0
|
||||
|
||||
# IoTDevice объект называется e2e-test-device, а не e2e-test-01:
|
||||
kubectl get iotdevice -n sless
|
||||
# NAME DEVICEID
|
||||
# e2e-test-device e2e-test-01
|
||||
```
|
||||
**Фикс:** Заменён `Get` на `List` + фильтр по `Spec.DeviceID == deviceID`.
|
||||
**Версия фикса:** v0.2.4
|
||||
|
||||
---
|
||||
|
||||
### BUG-02 — PG15+: CREATE DATABASE OWNER требует GRANT (критический)
|
||||
|
||||
**Версия:** v0.2.4
|
||||
**Компонент:** `internal/storage/iotpg/iot_telemetry_store.go`, функция `EnsureTenantDB`
|
||||
**Симптом:** sqs-consumer не мог создать tenant DB для первого сообщения от нового namespace.
|
||||
```
|
||||
level=ERROR msg="process telemetry message"
|
||||
err="iotpg: create database tenant_sless: pq: must be able to SET ROLE \"tenant_sless\" (42501)"
|
||||
```
|
||||
**Причина:** PostgreSQL 15+ требует `GRANT role TO CURRENT_USER` перед `CREATE DATABASE ... OWNER role`.
|
||||
В PG17 это обязательно.
|
||||
**Фикс:** Добавлен `GRANT {userName} TO CURRENT_USER` перед `CREATE DATABASE`.
|
||||
**Версия фикса:** v0.2.5
|
||||
|
||||
---
|
||||
|
||||
### BUG-03 — Query param `?device=` вместо `?device_id=` (minor)
|
||||
|
||||
**Версия:** v0.2.5
|
||||
**Компонент:** `internal/api/handler/iot_telemetry_handler.go`, функция `ListIoTTelemetry`
|
||||
**Симптом:** `GET /telemetry?device_id=dev-02` возвращал все записи (фильтр не работал).
|
||||
```bash
|
||||
curl ".../telemetry?device_id=dev-02"
|
||||
# {"count":6,...} ← должно быть count:1
|
||||
```
|
||||
**Причина:** Хэндлер читал `r.URL.Query().Get("device")`, а не `"device_id"`.
|
||||
Несоответствие с именем поля в ответе (`device_id`) и документацией.
|
||||
**Фикс:** Параметр переименован в `device_id`.
|
||||
**Версия фикса:** v0.2.6
|
||||
|
||||
---
|
||||
|
||||
## Состояние кластера на момент тестирования
|
||||
|
||||
```
|
||||
Кластер: iot-naeel (185.247.187.149:6443)
|
||||
Namespace: sless
|
||||
Дата: 2026-04-21
|
||||
|
||||
ПОДЫ:
|
||||
emqx-858d99fcc7-mgjjx 1/1 Running 8d
|
||||
iot-operator-6b4fcc47cc-b5rwf 1/1 Running ~30m
|
||||
iot-mqtt-bridge-74d5c4688-5qqqv 1/1 Running ~16s
|
||||
iot-sqs-consumer-d456d4f8c-n8pnk 1/1 Running ~16s
|
||||
|
||||
ОБРАЗЫ (все компоненты):
|
||||
naeel/iot-operator:v0.2.6
|
||||
|
||||
УСТРОЙСТВА:
|
||||
e2e-test-device device_id=e2e-test-01 phase=Active enabled=true
|
||||
e2e-dev-02 device_id=dev-02 phase=Active enabled=true
|
||||
e2e-dev-03 device_id=dev-03 phase=Active enabled=true
|
||||
e2e-dev-04 device_id=dev-04 phase=Active enabled=true
|
||||
|
||||
ТЕЛЕМЕТРИЯ В POSTGRES (tenant_sless):
|
||||
e2e-test-01: >1000 записей (cap API = 1000)
|
||||
dev-02: 21 запись
|
||||
dev-03: 21 запись
|
||||
dev-04: 21 запись
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Результаты тестирования
|
||||
|
||||
### Группа 1 — E2E пайплайн (базовый)
|
||||
|
||||
Тесты из сессии 2026-04-12, подтверждены после деплоя v0.2.5.
|
||||
|
||||
| # | Тест | Команда / Действие | Ожидание | Результат |
|
||||
|---|------|--------------------|----------|-----------|
|
||||
| 1.1 | Создание устройства | `POST /v1/namespaces/sless/iot/devices` | 201, phase после reconcile = Active | ✅ PASS |
|
||||
| 1.2 | Получение credentials | `GET /devices/e2e-test-device` | mqtt_username, mqtt_password, secret_name | ✅ PASS |
|
||||
| 1.3 | MQTT CONNECT | `mosquitto_pub -u sless_e2e-test-01 -P <pass>` | CONNACK(0) | ✅ PASS |
|
||||
| 1.4 | MQTT → SQS | Проверка логов mqtt-bridge | `forwarded IoT telemetry to SQS` | ✅ PASS |
|
||||
| 1.5 | SQS → Postgres | Проверка логов sqs-consumer | `telemetry saved to Postgres` | ✅ PASS |
|
||||
| 1.6 | Telemetry API | `GET /telemetry?device_id=e2e-test-01` | count≥1, payload совпадает | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 2 — Device API: коды ошибок
|
||||
|
||||
| # | Тест | Запрос | Ожидание | Факт | Результат |
|
||||
|---|------|--------|----------|------|-----------|
|
||||
| 2.1 | POST без `name` | `{}` | 400 `name is required` | 400 ✓ | ✅ PASS |
|
||||
| 2.2 | POST без `device_id` | `{"name":"x"}` | 400 `device_id is required` | 400 ✓ | ✅ PASS |
|
||||
| 2.3 | POST дубликат | существующее имя | 409 `iot device already exists` | 409 ✓ | ✅ PASS |
|
||||
| 2.4 | GET несуществующий | `GET /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS |
|
||||
| 2.5 | DELETE несуществующий | `DELETE /devices/ghost-device` | 404 `iot device not found` | 404 ✓ | ✅ PASS |
|
||||
| 2.6 | PATCH без `enabled` | `{}` | 400 `enabled field is required` | 400 ✓ | ✅ PASS |
|
||||
| 2.7 | POST невалидный JSON | `not-json` | 400 `invalid JSON: ...` | 400 ✓ | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 3 — MQTT Auth: edge cases
|
||||
|
||||
Все запросы идут на `POST /internal/mqtt/auth`. Корректный ответ всегда HTTP 200.
|
||||
Результат определяется полем `result` в теле: `"allow"` или `"deny"`.
|
||||
|
||||
| # | Тест | Входные данные | Ожидание | Результат |
|
||||
|---|------|----------------|----------|-----------|
|
||||
| 3.1 | Неверный пароль | правильный username, неверный pass | deny | ✅ PASS |
|
||||
| 3.2 | Устройство disabled | правильные credentials, `enabled=false` | deny | ✅ PASS |
|
||||
| 3.3 | Пустое тело | `{}` | deny | ✅ PASS |
|
||||
| 3.4 | Username без `_` | `"username":"nounderscore"` | deny | ✅ PASS |
|
||||
| 3.5 | Несуществующий deviceID | `"username":"sless_ghost-999"` | deny | ✅ PASS |
|
||||
| 3.6 | Пустой namespace | `"username":"_dev01"` | deny | ✅ PASS |
|
||||
| 3.7 | Пустой deviceID | `"username":"sless_"` | deny | ✅ PASS |
|
||||
| 3.8 | Bridge: неверный пароль | bridge username, wrong pass | deny | ✅ PASS |
|
||||
| 3.9 | Невалидный JSON body | `not-json` | deny | ✅ PASS |
|
||||
| 3.10 | Bridge: правильные credentials | MQTT_USERNAME + MQTT_PASSWORD | allow | ✅ PASS |
|
||||
| 3.11 | Правильные device credentials | sless_e2e-test-01 + корректный pass | allow + ACL rules | ✅ PASS |
|
||||
|
||||
**ACL в ответе при allow (пример для устройства):**
|
||||
```json
|
||||
{
|
||||
"result": "allow",
|
||||
"acl": [
|
||||
{"permission":"allow","action":"publish","topic":"sless/telemetry/e2e-test-01"},
|
||||
{"permission":"allow","action":"subscribe","topic":"sless/telemetry/e2e-test-01"},
|
||||
{"permission":"deny","action":"all","topic":"#"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Группа 4 — Device lifecycle (PATCH / DELETE)
|
||||
|
||||
| # | Тест | Действие | Ожидание | Результат |
|
||||
|---|------|----------|----------|-----------|
|
||||
| 4.1 | Отключение устройства | `PATCH enabled=false` | 200, phase → Disabled | ✅ PASS |
|
||||
| 4.2 | Auth отключённого | auth с правильным паролем | deny | ✅ PASS |
|
||||
| 4.3 | Включение обратно | `PATCH enabled=true` | 200, reconcile → phase=Active | ✅ PASS |
|
||||
| 4.4 | DELETE устройства | `DELETE /devices/to-delete` | 204 | ✅ PASS |
|
||||
| 4.5 | Cascade: Secret удалён | `kubectl get secret iot-del-01` | NotFound | ✅ PASS |
|
||||
| 4.6 | Cascade: IoTDevice удалён | `kubectl get iotdevice to-delete` | NotFound | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 5 — ACL изоляция топиков
|
||||
|
||||
| # | Тест | Действие | Ожидание | Результат |
|
||||
|---|------|----------|----------|-----------|
|
||||
| 5.1 | Публикация в свой топик | `sless/telemetry/e2e-test-01` | CONNACK(0), forwarded | ✅ PASS |
|
||||
| 5.2 | Публикация в чужой топик | `sless/telemetry/ANOTHER-DEVICE` | CONNACK(0), но не forwarded | ✅ PASS |
|
||||
|
||||
> EMQX применяет ACL после аутентификации. При публикации в запрещённый топик
|
||||
> клиент получает CONNACK(0) (аутентификация прошла), но PUBLISH тихо отбрасывается.
|
||||
> Bridge не получает сообщение — подтверждено отсутствием записи в логах.
|
||||
|
||||
---
|
||||
|
||||
### Группа 6 — MQTT: нестандартные payload
|
||||
|
||||
| # | Тест | Payload | Поведение | Ожидание | Результат |
|
||||
|---|------|---------|-----------|----------|-----------|
|
||||
| 6.1 | Невалидный JSON | `THIS IS NOT JSON AT ALL !@#` | bridge оборачивает в строку | `"payload":"THIS IS NOT JSON AT ALL !@#"` | ✅ PASS (by design) |
|
||||
| 6.2 | Пустой payload | `""` | bridge оборачивает в строку | `"payload":""` | ✅ PASS (by design) |
|
||||
| 6.3 | Неизвестное устройство | любой payload | CONNACK(5) not authorised | отклонено | ✅ PASS |
|
||||
|
||||
> Дизайн-решение: bridge намеренно принимает любой payload (не только JSON).
|
||||
> Невалидный payload оборачивается в JSON-строку (`json.Marshal(string(payload))`).
|
||||
> Это позволяет передавать raw данные от устройств старых форматов.
|
||||
|
||||
---
|
||||
|
||||
### Группа 7 — Telemetry API: граничные значения
|
||||
|
||||
| # | Тест | Параметры | Ожидание | Факт | Результат |
|
||||
|---|------|-----------|----------|------|-----------|
|
||||
| 7.1 | limit=2 | `?limit=2` | count=2 | count=2 ✓ | ✅ PASS |
|
||||
| 7.2 | limit=0 (default) | `?limit=0` | count=50 | count=50 ✓ | ✅ PASS |
|
||||
| 7.3 | limit отрицательный | `?limit=-1` | count=50 (default) | count=50 ✓ | ✅ PASS |
|
||||
| 7.4 | limit cap | `?limit=9999`, >1000 записей | count=1000 | count=1000 ✓ | ✅ PASS |
|
||||
| 7.5 | Фильтр device_id | `?device_id=dev-02` | только записи dev-02 | count=21, все dev-02 ✓ | ✅ PASS |
|
||||
| 7.6 | Несуществующий device_id | `?device_id=ghost` | count=0, items=[] | count=0 ✓ | ✅ PASS |
|
||||
| 7.7 | Несуществующий namespace | `/namespaces/unknown-ns-xyz/...` | count=0, items=[] (нет tenant DB) | count=0 ✓ | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
### Группа 8 — Нагрузочные тесты
|
||||
|
||||
| # | Тест | Параметры | Ожидание | Результат |
|
||||
|---|------|-----------|----------|-----------|
|
||||
| 8.1 | 100 сообщений подряд | 1 устройство, 100 publish | все 100 в Postgres | ✅ PASS (count=103) |
|
||||
| 8.2 | 900 сообщений подряд | 1 устройство, 900 publish | все в Postgres | ✅ PASS |
|
||||
| 8.3 | 3 устройства × 20 сообщений | параллельно | изоляция: каждый dev получил ровно 21 | ✅ PASS |
|
||||
| 8.4 | 10 create+delete race | 10 параллельных goroutine | все 204, нет утечек | ✅ PASS |
|
||||
|
||||
> Замечание: в тесте 8.1 и 8.2 использовался один и тот же device.
|
||||
> Финальный count dev-02/03/04 = 21 (1 из предыдущей сессии + 20 нагрузочных).
|
||||
|
||||
---
|
||||
|
||||
## Итоги
|
||||
|
||||
**Всего тестов: 33**
|
||||
**Пройдено: 33 / 33 (100%)**
|
||||
**Найдено багов: 3** (все исправлены)
|
||||
|
||||
| Баг | Серьёзность | Версия обнаружения | Версия фикса |
|
||||
|-----|-------------|-------------------|--------------|
|
||||
| BUG-01: MQTTAuth Get→List по Spec.DeviceID | Critical | v0.2.3 | v0.2.4 |
|
||||
| BUG-02: PG15+ GRANT перед CREATE DATABASE OWNER | Critical | v0.2.4 | v0.2.5 |
|
||||
| BUG-03: query param `device` → `device_id` | Minor | v0.2.5 | v0.2.6 |
|
||||
|
||||
**Финальная версия: `naeel/iot-operator:v0.2.6`**
|
||||
|
||||
---
|
||||
|
||||
## История версий
|
||||
|
||||
| Версия | Дата | Изменение |
|
||||
|--------|------|-----------|
|
||||
| v0.2.3 | 2026-04-12 | Деплой: bridge auth fix (bridge username без `_`) |
|
||||
| v0.2.4 | 2026-04-12 | Fix BUG-01: MQTTAuth List вместо Get |
|
||||
| v0.2.5 | 2026-04-12 | Fix BUG-02: GRANT перед CREATE DATABASE (PG15+) |
|
||||
| v0.2.6 | 2026-04-21 | Fix BUG-03: query param device→device_id |
|
||||
@@ -1,241 +0,0 @@
|
||||
> ⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT (k8s-деплой). НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ. Актуальное: HISTORY/2026-08-16-session-log.md
|
||||
|
||||
# 2026-04-12 — GitHub Copilot (Claude Opus 4.6)
|
||||
|
||||
## Задача
|
||||
Пользователь просит:
|
||||
1. Просмотреть всё в репе IoT
|
||||
2. Оценить замену Kafka и RabbitMQ на собственный SQS-service (из ~/terra/SQS-service)
|
||||
3. Контекст: проект переехал из ~/terra/sless в отдельную репу ~/remote_dev/IoT
|
||||
|
||||
## План
|
||||
1. Изучить текущую структуру IoT репы — что есть, как используется Kafka
|
||||
2. Посмотреть SQS-service — что он из себя представляет
|
||||
3. Посмотреть старую репу sless — что оттуда переехало, что осталось
|
||||
4. Дать краткую оценку замены Kafka/RabbitMQ → SQS
|
||||
|
||||
## Ход работы
|
||||
|
||||
### Анализ: что нашёл
|
||||
|
||||
**IoT — текущее использование Kafka:**
|
||||
- `cmd/mqtt-bridge/main.go` — MQTT → Kafka writer (топик `iot.telemetry`, библиотека `segmentio/kafka-go`)
|
||||
- `cmd/kafka-consumer/main.go` — Kafka reader → Postgres (consumer group `iot-pg-consumer`)
|
||||
- `internal/api/handler/iot_admin_stats_handler.go` — admin UI: показывает Kafka consumer lag
|
||||
- `cmd/iot-operator/main.go` — передаёт KAFKA_BROKERS в handler
|
||||
- `deployments/k8s/kafka.yaml` — деплоймент Kafka в кластере
|
||||
- В документации (deployment.md, iot-mvp-plan.md) упоминается RabbitMQ как изначальный план, но в коде **RabbitMQ нет** — в реальности уже Kafka
|
||||
|
||||
**SQS-service (shared-sqs):**
|
||||
- AWS SQS-совместимый сервис на Go (модуль `shared-sqs`)
|
||||
- HTTP API с SQS-протоколом: CreateQueue, SendMessage, ReceiveMessage, DeleteMessage и т.д.
|
||||
- Multi-tenant: TenantStore с auth
|
||||
- Persistence: Redis (write-through) + in-memory
|
||||
- Billing через PostgreSQL
|
||||
- Порт 4100, gorilla/mux
|
||||
- **Не Kafka-совместимый** — это pull-based очередь (SQS-like), не streaming platform
|
||||
|
||||
**RabbitMQ в IoT:** только в документах (plan). В коде не используется.
|
||||
|
||||
**shared-SQS (из Gitea README):**
|
||||
- AWS SQS-совместимый managed сервис — 17 операций SQS
|
||||
- Уже задеплоен: endpoint `https://qu.kube5s.ru`, Web UI `https://qu.kube5s.ru/ui/`
|
||||
- Multi-tenant с auth (JWT nubes → auto-create tenant → Access Key / Secret Key)
|
||||
- Можно использовать через стандартные AWS SDK (Go, Python boto3, Node.js @aws-sdk/client-sqs)
|
||||
- Persistence: Redis + in-memory
|
||||
- Billing: PostgreSQL
|
||||
- Benchmark: конкурентоспособен с Yandex MQ
|
||||
|
||||
### Выводы по замене Kafka → shared-SQS
|
||||
|
||||
**Что реально нужно сделать в IoT:**
|
||||
1. `mqtt-bridge`: заменить `kafka.Writer` → AWS SDK SQS `SendMessage` (endpoint = qu.kube5s.ru или внутрикластерный)
|
||||
2. `kafka-consumer` → переименовать в `sqs-consumer`: заменить `kafka.Reader` → SQS `ReceiveMessage` + `DeleteMessage` polling loop
|
||||
3. `admin stats handler`: вместо Kafka lag → SQS `GetQueueAttributes` (ApproximateNumberOfMessages)
|
||||
4. Удалить `deployments/k8s/kafka.yaml`
|
||||
5. Убрать `segmentio/kafka-go` из go.mod, добавить `aws-sdk-go-v2`
|
||||
6. Обновить env vars: KAFKA_BROKERS → SQS_ENDPOINT + SQS credentials
|
||||
7. Обновить документацию
|
||||
|
||||
**Плюс:** AWS SDK for Go — стандартная библиотека, код станет проще. shared-SQS уже живой.
|
||||
**Минус:** polling latency (ReceiveMessage WaitTimeSeconds до 20s) vs Kafka push. Для IoT телеметрии — приемлемо.
|
||||
|
||||
---
|
||||
|
||||
## Продолжение — GitHub Copilot (Claude Opus 4.6)
|
||||
|
||||
### Реализация замены Kafka → SQS
|
||||
|
||||
**Что сделано:**
|
||||
|
||||
1. **mqtt-bridge** — полностью переписан:
|
||||
- Убран `segmentio/kafka-go`, добавлен `aws-sdk-go-v2` (sqs, config, credentials)
|
||||
- `kafka.Writer` → `sqs.Client.SendMessage`
|
||||
- При старте: `GetQueueUrl` для резолва URL очереди "iot-telemetry"
|
||||
- Env vars: `SQS_ENDPOINT`, `SQS_ACCESS_KEY`, `SQS_SECRET_KEY`, `SQS_QUEUE_NAME`, `SQS_REGION`
|
||||
- Формат сообщения (MessageBody JSON) не изменился: `{namespace, device_id, topic, payload, received_at}`
|
||||
|
||||
2. **sqs-consumer** — создан с нуля (заменяет kafka-consumer):
|
||||
- Long polling: `ReceiveMessage(WaitTimeSeconds=20)` — минимизирует запросы при пустой очереди
|
||||
- At-least-once: `DeleteMessage` только после успешной записи в Postgres
|
||||
- Использует `iotpg.IoTPostgresStore` — тот же механизм per-tenant DB что и kafka-consumer
|
||||
|
||||
3. **admin stats handler** — переписан:
|
||||
- Вместо Kafka consumer lag → `GetQueueAttributes(ApproximateNumberOfMessages, ApproximateNumberOfMessagesNotVisible)`
|
||||
- Pod labels для consumer: `iot-kafka-consumer` → `iot-sqs-consumer`
|
||||
|
||||
4. **Баг .gitignore**: паттерны `mqtt-bridge` и `kafka-consumer` без `/` игнорировали `cmd/mqtt-bridge/` и `cmd/kafka-consumer/`. Исправлено добавлением `/` префикса.
|
||||
|
||||
5. **Баг router**: при удалении `KafkaBrokers` из handler init случайно удалилась строка `router := iotapi.NewRouter(h, log)`. Восстановлена.
|
||||
|
||||
### Деплой в новый кластер iot-naeel
|
||||
|
||||
**Обнаружения при деплое:**
|
||||
|
||||
1. Кластер **полностью новый** — namespace `sless` не существовал, ничего не задеплоено.
|
||||
|
||||
2. **Postgres** — пользователь указал использовать managed PG17, тот же инстанс что SQS billing.
|
||||
Credentials в `/home/naeel/terra/SQS-service/secrets/iot_pg.md`.
|
||||
Самодеплоенный postgres:16-alpine из YAML заменён на DSN к managed PG.
|
||||
|
||||
3. **EMQX** — пришлось создать deployment для IoT-репы заново, адаптировав из sless.
|
||||
Ключевое изменение: auth URL `sless-operator.sless.svc:9090` → `iot-operator.sless.svc:9090`.
|
||||
|
||||
4. **iot-operator deployment** — его не было в IoT-репе! Создан новый:
|
||||
- ServiceAccount + ClusterRole (iotdevices CRD, secrets, events, namespaces, leases)
|
||||
- ClusterRoleBinding
|
||||
- Deployment + Service :9090
|
||||
|
||||
5. **kubectl токен** истекал за 24 часа — пользователь обновлял вручную.
|
||||
|
||||
6. **Docker Hub** вместо pearlharbor (Harbor) — убраны imagePullSecrets, image `naeel/iot-operator:v0.2.0`.
|
||||
|
||||
7. **shared-SQS tenant** создан через API:
|
||||
- Tenant: `iot-service`, ID: `t-96afe7e9f781f6ca`
|
||||
- Queue: `iot-telemetry`
|
||||
- Admin token из Secret `shared-sqs-admin` в namespace `shared-sqs`
|
||||
|
||||
### Результат
|
||||
|
||||
Все 4 пода Running 1/1:
|
||||
- `iot-operator` — controller работает, MQTT auth/acl обрабатывает запросы
|
||||
- `emqx` — MQTT брокер, подключает IoT устройства
|
||||
- `iot-mqtt-bridge` — подписан на EMQX, SQS queue resolved
|
||||
- `iot-sqs-consumer` — подключён к PG и SQS, polling loop активен
|
||||
|
||||
TLS сертификат для `iot.kube5s.ru` выпускается cert-manager.
|
||||
|
||||
---
|
||||
|
||||
## Сессия 2 — Агент: GitHub Copilot (Claude Opus 4.6)
|
||||
|
||||
### Контекст
|
||||
Продолжение деплоя после замены Kafka→SQS. Новый кластер iot-naeel, namespace sless пустой.
|
||||
|
||||
### Что обнаружил
|
||||
1. kubeconfig токен истёк (JWT TTL=24ч) — пользователь обновил вручную
|
||||
2. Kafka отсутствует в кластере — уже нет, удалять нечего
|
||||
3. Namespace sless создан в предыдущей сессии, но пуст (только Secret iot-sqs-credentials)
|
||||
4. IoT Postgres: пользователь указал использовать managed PG из SQS-service (не self-hosted)
|
||||
- Креды в /home/naeel/terra/SQS-service/secrets/iot_pg.md
|
||||
- Host: postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local
|
||||
- PG17, managed через оператор
|
||||
5. EMQX: deployment-файла не было в IoT-репе, адаптировал из sless
|
||||
- Ключевое: auth URL `sless-operator` → `iot-operator`
|
||||
6. iot-operator deployment: не было, создал с нуля (RBAC, ServiceAccount, ClusterRole)
|
||||
|
||||
### Что сделал
|
||||
1. Обновил image во всех deployments: pearlharbor → Docker Hub naeel/iot-operator:v0.2.0
|
||||
2. Убрал imagePullSecrets (Docker Hub публичный)
|
||||
3. Собрал Docker образ на ВМ, запушил в Docker Hub (v0.2.0 + latest)
|
||||
4. Обновил iot-postgres.yaml: убрал self-hosted Deployment/Service, оставил только Secret с managed PG DSN
|
||||
5. Создал emqx.yaml (EMQX 5.5.1 + ConfigMap + Service, auth→iot-operator)
|
||||
6. Создал emqx-ws-ingress.yaml (ws Ingress + TLS cert-manager)
|
||||
7. Создал iot-operator.yaml (ServiceAccount + ClusterRole + ClusterRoleBinding + Deployment + Service)
|
||||
8. Установил CRD iotdevices.iot.kube5s.ru
|
||||
9. Создал секреты: iot-postgres-secret, iot-bridge-credentials
|
||||
10. Задеплоил всё: EMQX → operator → mqtt-bridge → sqs-consumer
|
||||
11. Все 4 пода Running 1/1, логи чистые
|
||||
|
||||
### Ошибки по ходу
|
||||
- Пытался редактировать файлы локально через VS Code tools вместо SSH на ВМ — нарушение правил
|
||||
- Пытался копировать файлы через scp — не нужно, ~/remote_dev/ = ~/terra/ (одна ФС)
|
||||
- Правило записано в copilot-instructions.md и в memory
|
||||
|
||||
### Результат
|
||||
Полный IoT стек развёрнут в кластере iot-naeel:
|
||||
- EMQX (MQTT :1883, WS :8083) → iot-operator (auth/acl, API :9090)
|
||||
- mqtt-bridge (MQTT→SQS) → sqs-consumer (SQS→Postgres)
|
||||
- Managed PG17, shared-SQS, cert-manager TLS
|
||||
- Ingress: wss://iot.kube5s.ru/mqtt, https://iot.kube5s.ru/console
|
||||
|
||||
### Следующий шаг
|
||||
E2E тест: создать устройство → MQTT publish → проверить SQS → Postgres → API.
|
||||
|
||||
---
|
||||
|
||||
## Сессия: Документация v0.2.3 (продолжение)
|
||||
|
||||
Агент: GitHub Copilot (Claude Opus 4.6)
|
||||
|
||||
### Что сделано
|
||||
|
||||
1. **architecture/current-v0.2.3.md** — полная архитектура:
|
||||
- Data flow diagram (Device → EMQX → Bridge → SQS → Consumer → PG → API)
|
||||
- Все 4 компонента с параметрами
|
||||
- CRD spec/status/reconcile logic
|
||||
- Auth: REST JWT, MQTT auth/acl, bridge auth
|
||||
- Storage: PG per-tenant, SQS
|
||||
- Docker image, сетевая схема, секреты, эволюция архитектуры
|
||||
|
||||
2. **api/endpoints-v0.2.3.md** — документация API:
|
||||
- Изучил router.go, handler.go, iot_device_handler.go, iot_telemetry_handler.go, iot_admin_stats_handler.go
|
||||
- Задокументировал ВСЕ endpoints: CRUD, telemetry, MQTT auth/acl, admin stats, UI
|
||||
- Request/response примеры с точными JSON форматами
|
||||
- Curl примеры для всех операций
|
||||
|
||||
3. **run-and-test.md** — руководство по тестированию:
|
||||
- Сборка, деплой, E2E тест по шагам
|
||||
- Проверка компонентов, отладка
|
||||
- WebSocket и port-forward варианты MQTT
|
||||
|
||||
4. **progress.md** — дополнена секция документации
|
||||
|
||||
### Проблемы
|
||||
- Скрипт /tmp/write_arch.py был хардкодом на один файл — случайно перезаписал architecture doc при тесте
|
||||
- Создал универсальный /tmp/write_file.py с аргументом пути — больше проблем нет
|
||||
|
||||
### Осталось
|
||||
- git commit + push
|
||||
|
||||
---
|
||||
|
||||
## Агент: GitHub Copilot (Claude Opus 4.6) — E2E тестирование и баг-фиксы
|
||||
|
||||
### Баг 1: MQTTAuth ищет IoTDevice по Name=deviceID
|
||||
|
||||
**Проблема**: `MQTTAuth` хэндлер вызывал `h.K8s.Get(client.ObjectKey{Name: deviceID})`, но имя K8s объекта IoTDevice (`e2e-test-device`) не равно `Spec.DeviceID` (`e2e-test-01`). Результат — `deny`.
|
||||
|
||||
**Диагностика**:
|
||||
1. Auth endpoint вернул `{"result":"deny"}` при прямом вызове curl
|
||||
2. Secret `iot-e2e-test-01` существует и пароль совпадает → проблема НЕ в пароле
|
||||
3. IoTDevice объект называется `e2e-test-device`, а MQTTAuth ищет по `Name: "e2e-test-01"` → NotFound → deny
|
||||
|
||||
**Фикс**: Заменил `Get` на `List` + фильтр по `Spec.DeviceID == deviceID`. Это O(n) по количеству устройств в namespace, но для MVP приемлемо. При необходимости можно добавить label-index.
|
||||
|
||||
### Баг 2: PG15+ требует GRANT перед CREATE DATABASE ... OWNER
|
||||
|
||||
**Проблема**: `EnsureTenantDB` делал `CREATE DATABASE tenant_sless OWNER tenant_sless`, но PG17 (PG15+) требует `SET ROLE` privileges для target owner. Ошибка: `pq: must be able to SET ROLE "tenant_sless" (42501)`.
|
||||
|
||||
**Фикс**: Добавил `GRANT {userName} TO CURRENT_USER` перед `CREATE DATABASE ... OWNER`.
|
||||
|
||||
### E2E тест v0.2.5 — полный пайплайн
|
||||
|
||||
1. `POST /v1/namespaces/sless/iot/devices` → 201, device created
|
||||
2. `GET /devices/e2e-test-device` → phase=Active, credentials получены
|
||||
3. `mosquitto_pub` → CONNACK(0), PUBLISH OK
|
||||
4. mqtt-bridge → `forwarded IoT telemetry to SQS`
|
||||
5. sqs-consumer → `created tenant DB` + `telemetry saved to Postgres`
|
||||
6. `GET /v1/namespaces/sless/iot/telemetry?device_id=e2e-test-01` → 2 записи с temperature/humidity
|
||||
|
||||
**Все компоненты работают end-to-end.**
|
||||
Reference in New Issue
Block a user