refactor: старые артефакты IoT перенесены в legacy/ (git mv, история сохранена); активное дерево очищено, go build OK

This commit is contained in:
“Naeel”
2026-08-16 09:48:34 +04:00
parent bf32394def
commit f0c36a11ee
30 changed files with 31 additions and 9 deletions
-412
View File
@@ -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"
```
-56
View File
@@ -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` — количество сообщений в очереди, ожидающих обработки.
-258
View File
@@ -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 |
-236
View File
@@ -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`. Если есть внутрикластерный сервис — обновим.
-43
View File
@@ -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 могут требовать обновления (новое имя образа)
-297
View File
@@ -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`
-406
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-141
View File
@@ -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`
-251
View File
@@ -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 секунд
-537
View File
@@ -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 |
-241
View File
@@ -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.**