fix: MQTTAuth device lookup by Spec.DeviceID + PG15+ GRANT (v0.2.5)
- MQTTAuth: replaced Get(Name=deviceID) with List+filter by Spec.DeviceID (K8s object name != deviceID — caused deny for all devices) - EnsureTenantDB: added GRANT role TO CURRENT_USER before CREATE DATABASE OWNER (PG15+ requires SET ROLE privileges for target owner) - Image: naeel/iot-operator:v0.2.5 - E2E test passed: device create → MQTT publish → SQS → Postgres → REST API
This commit is contained in:
@@ -38,7 +38,7 @@ spec:
|
||||
- name: mqtt-bridge
|
||||
# Тот же образ что и оператор — оба бинаря в одном слое (manager + iot-mqtt-bridge).
|
||||
# При смене версии оператора — менять тег и здесь.
|
||||
image: naeel/iot-operator:v0.2.3
|
||||
image: naeel/iot-operator:v0.2.5
|
||||
imagePullPolicy: Always
|
||||
command: ["/mqtt-bridge"]
|
||||
env:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# Компоненты:
|
||||
# - ServiceAccount + ClusterRole + ClusterRoleBinding (RBAC для CRD controller)
|
||||
# - Deployment: naeel/iot-operator:v0.2.3
|
||||
# - Deployment: naeel/iot-operator:v0.2.5
|
||||
# - Service: ClusterIP :9090 (REST API, MQTT auth/acl, admin UI)
|
||||
#
|
||||
# iot-operator обслуживает:
|
||||
@@ -85,7 +85,7 @@ spec:
|
||||
serviceAccountName: iot-operator
|
||||
containers:
|
||||
- name: operator
|
||||
image: naeel/iot-operator:v0.2.3
|
||||
image: naeel/iot-operator:v0.2.5
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: api
|
||||
|
||||
@@ -36,7 +36,7 @@ spec:
|
||||
containers:
|
||||
- name: sqs-consumer
|
||||
# Тот же образ что и оператор — все IoT бинари в одном образе.
|
||||
image: naeel/iot-operator:v0.2.3
|
||||
image: naeel/iot-operator:v0.2.5
|
||||
imagePullPolicy: Always
|
||||
command: ["/sqs-consumer"]
|
||||
env:
|
||||
|
||||
@@ -122,3 +122,18 @@
|
||||
- [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`
|
||||
|
||||
@@ -205,3 +205,35 @@ E2E тест: создать устройство → MQTT publish → пров
|
||||
|
||||
### Осталось
|
||||
- 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.**
|
||||
|
||||
@@ -193,10 +193,21 @@ func (h *Handler) MQTTAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем что IoTDevice активно
|
||||
device := &iotv1alpha1.IoTDevice{}
|
||||
if err := h.K8s.Get(r.Context(), client.ObjectKey{Namespace: ns, Name: deviceID}, device); err != nil {
|
||||
// IoTDevice не найден (или удалён) — deny
|
||||
// Ищем IoTDevice по Spec.DeviceID (имя K8s объекта может отличаться от deviceID)
|
||||
deviceList := &iotv1alpha1.IoTDeviceList{}
|
||||
if err := h.K8s.List(r.Context(), deviceList, client.InNamespace(ns)); err != nil {
|
||||
writeJSON(w, http.StatusOK, mqttAuthResponse{Result: "deny"})
|
||||
return
|
||||
}
|
||||
var device *iotv1alpha1.IoTDevice
|
||||
for i := range deviceList.Items {
|
||||
if deviceList.Items[i].Spec.DeviceID == deviceID {
|
||||
device = &deviceList.Items[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if device == nil {
|
||||
// IoTDevice с таким deviceID не найден — deny
|
||||
writeJSON(w, http.StatusOK, mqttAuthResponse{Result: "deny"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,6 +126,13 @@ func (s *IoTPostgresStore) EnsureTenantDB(ctx context.Context, namespace string)
|
||||
return fmt.Errorf("iotpg: create user %s: %w", userName, err)
|
||||
}
|
||||
|
||||
// PG15+: GRANT role TO current_user перед CREATE DATABASE ... OWNER
|
||||
if _, err = s.adminDB.ExecContext(ctx,
|
||||
fmt.Sprintf(`GRANT %s TO CURRENT_USER`, userName),
|
||||
); err != nil {
|
||||
return fmt.Errorf("iotpg: grant role %s: %w", userName, err)
|
||||
}
|
||||
|
||||
// CREATE DATABASE нельзя в транзакции
|
||||
if _, err = s.adminDB.ExecContext(ctx,
|
||||
fmt.Sprintf(`CREATE DATABASE %s OWNER %s`, dbName, userName),
|
||||
|
||||
Reference in New Issue
Block a user