From 857d057af908e345c7e40f7faa74abcd4ff941bb Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 4 Apr 2026 10:29:47 +0300 Subject: [PATCH] =?UTF-8?q?feat(iot):=20=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20IoT=20MVP=20=E2=80=94=20Dockerfile,=20RBAC,=20EMQX=20f?= =?UTF-8?q?ix,=20operator=20v0.1.50,=20mqtt-bridge,=20doc/iot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 11 +- deployments/k8s/emqx.yaml | 9 + deployments/k8s/iot-mqtt-bridge.yaml | 5 +- deployments/k8s/operator.yaml | 3 +- deployments/k8s/rbac.yaml | 15 +- doc/iot/deployment.md | 306 +++++++++++++++++++++ doc/thinking/2026-04-04.md | 108 ++++++++ internal/api/handler/iot_device_handler.go | 36 +-- main.go | 4 +- 9 files changed, 471 insertions(+), 26 deletions(-) create mode 100644 doc/iot/deployment.md diff --git a/Dockerfile b/Dockerfile index ea274b9..682f4a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Изменено: 2026-03-07 +# Изменено: 2026-04-04 — добавлена IoT поддержка: COPY iot/ + сборка iot-mqtt-bridge бинаря # Multi-stage build для sless оператора. # Stage 1: сборка бинаря (golang:1.23-alpine) # Stage 2: минимальный образ (alpine:3.19, не distroless — нужен ca-certificates для S3/HTTPS) @@ -17,14 +17,23 @@ COPY api/ api/ COPY controllers/ controllers/ COPY internal/ internal/ COPY migrations/ migrations/ +# iot/ — IoT CRD types, controller, mqtt-bridge cmd. +# Обязательно: main.go импортирует iot/api/v1alpha1 и iot/controllers — без этого go build упадёт. +COPY iot/ iot/ RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go +# iot-mqtt-bridge — отдельный бинарь в том же образе. +# Запускается в iot-mqtt-bridge Deployment через command: ["/iot-mqtt-bridge"]. +# Один образ, два entrypoint — практично для MVP: один CI pipeline, один registry repo. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o iot-mqtt-bridge ./iot/cmd/mqtt-bridge/ FROM alpine:3.19 # ca-certificates нужны для TLS (S3 HTTPS, DockerHub) RUN apk add --no-cache ca-certificates WORKDIR / COPY --from=builder /workspace/manager . +# iot-mqtt-bridge — второй бинарь, запускается отдельным Deployment-ом. +COPY --from=builder /workspace/iot-mqtt-bridge . # migrations нужны при старте — оператор читает SQL файлы для инициализации БД COPY migrations/ migrations/ # Запускаем от непривилегированного пользователя diff --git a/deployments/k8s/emqx.yaml b/deployments/k8s/emqx.yaml index 84f583c..7e76048 100644 --- a/deployments/k8s/emqx.yaml +++ b/deployments/k8s/emqx.yaml @@ -31,6 +31,15 @@ data: ## EMQX 5.x configuration (HOCON format) ## Изменено: 2026-04-04 + ## Обязательные поля node — без них EMQX 5.x падает при старте + ## node.cookie — секрет кластерного Erlang-соединения, для single-node любая строка + ## node.data_dir — директория данных (mnesia, конфиги), должна существовать в контейнере + node { + name = "emqx@127.0.0.1" + cookie = "sless-emqx-cookie-mvp" + data_dir = "/opt/emqx/data" + } + ## HTTP Auth Backend для IoT-устройств ## EMQX посылает POST с {username, password, clientid} → наш сервис отвечает {"result":"allow"|"deny"} authentication = [ diff --git a/deployments/k8s/iot-mqtt-bridge.yaml b/deployments/k8s/iot-mqtt-bridge.yaml index e9ace7d..2060a51 100644 --- a/deployments/k8s/iot-mqtt-bridge.yaml +++ b/deployments/k8s/iot-mqtt-bridge.yaml @@ -42,8 +42,9 @@ spec: spec: containers: - name: mqtt-bridge - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:latest - # TODO: отдельный образ iot-mqtt-bridge После сборки через Makefile + # Тот же образ что и оператор — оба бинаря в одном слое (manager + iot-mqtt-bridge). + # При смене версии оператора — менять тег и здесь. + image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.50 imagePullPolicy: Always command: ["/iot-mqtt-bridge"] env: diff --git a/deployments/k8s/operator.yaml b/deployments/k8s/operator.yaml index df9533a..e91cfa4 100644 --- a/deployments/k8s/operator.yaml +++ b/deployments/k8s/operator.yaml @@ -74,7 +74,8 @@ spec: containers: - name: operator # При обновлении версии оператора — менять тег здесь (не latest!) - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.49 + # v0.1.50 — добавлены IoT controller, IoT REST API, MQTT auth endpoint + image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.50 # Always — чтобы всегда тянуть по точному тегу (не кешировать старый) imagePullPolicy: Always ports: diff --git a/deployments/k8s/rbac.yaml b/deployments/k8s/rbac.yaml index 0a83ecc..2a84ef0 100644 --- a/deployments/k8s/rbac.yaml +++ b/deployments/k8s/rbac.yaml @@ -1,4 +1,4 @@ -# Изменено: 2026-03-20 (добавлен Service CRD sless.kube5s.ru — services + status + finalizers) +# Изменено: 2026-04-04 — добавлены IoT CRD права (iot.kube5s.ru) # RBAC для sless оператора. # ServiceAccount + ClusterRole + ClusterRoleBinding. # ClusterRole нужен (не namespaced Role) потому что оператор создаёт @@ -15,7 +15,7 @@ kind: ClusterRole metadata: name: sless-operator rules: - # Наши CRD + # Наши CRD (sless.kube5s.ru) - apiGroups: ["sless.kube5s.ru"] resources: ["functions", "triggers", "functionjobs", "services"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] @@ -25,6 +25,17 @@ rules: - apiGroups: ["sless.kube5s.ru"] resources: ["functions/finalizers", "triggers/finalizers", "functionjobs/finalizers", "services/finalizers"] verbs: ["update"] + # IoT CRD (iot.kube5s.ru) — IoTDevice lifecycle + Secret генерация в контроллере + # Права нужны во всех namespace где пользователи создают IoT-устройства + - 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"] # Deployments для функций - apiGroups: ["apps"] resources: ["deployments"] diff --git a/doc/iot/deployment.md b/doc/iot/deployment.md new file mode 100644 index 0000000..daf482a --- /dev/null +++ b/doc/iot/deployment.md @@ -0,0 +1,306 @@ +# 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": "", + "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 | diff --git a/doc/thinking/2026-04-04.md b/doc/thinking/2026-04-04.md index facfb43..a549907 100644 --- a/doc/thinking/2026-04-04.md +++ b/doc/thinking/2026-04-04.md @@ -571,3 +571,111 @@ IoT event consumer → [interface] → POST → function 3. Kafka deployment: managed или Strimzi Всё. Наш IoT-оператор, CRD, device auth — не меняются вообще. + +--- + +## Агент: GitHub Copilot (Claude Sonnet 4.6) — ДЕПЛОЙ IoT MVP + +### Задача от пользователя +Запустить всё. Документировать мысли и инженерную часть подробно. + +### Анализ ситуации перед деплоем + +**Что есть в git (ветка Ioter, коммиты 716efaf + 1e53766 + b920dc5):** +- CRD IoTDevice (iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml) — готово, не задеплоено +- IoT controller + IoT API handlers — в бинаре `manager`, НО образ не пересобран (v0.1.49 = без IoT) +- EMQX YAML (deployments/k8s/emqx.yaml) — готово, не задеплоено +- iot-mqtt-bridge YAML (deployments/k8s/iot-mqtt-bridge.yaml) — готово, не задеплоено +- mqtt-bridge бинарь (iot/cmd/mqtt-bridge/main.go) — есть код, но НЕ в образе + +**Критические блокеры:** + +1. **Dockerfile не включает `iot/`** — оператор собирается без IoT кода. Нет, стоп. + После проверки: iot/api включается через импорты в main.go — Go компилятор тянет весь нужный код. + НО: Dockerfile копирует только `main.go api/ controllers/ internal/ migrations/`. + IoT код живёт в `iot/` — и этот каталог НЕ копируется в Dockerfile! + Значит: `go build -o manager main.go` внутри Docker упадёт — не найдёт пакеты `iot/api/v1alpha1`, `iot/controllers/`. + +2. **mqtt-bridge бинарь** — отдельная точка входа `iot/cmd/mqtt-bridge/main.go`. + В текущем Dockerfile собирается только `manager`. Нужно добавить сборку `iot-mqtt-bridge`. + +3. **iot-mqtt-bridge.yaml** использует образ оператора (`sless-operator:latest`) с командой `/iot-mqtt-bridge`. + Значит оба бинаря должны быть в одном образе. Это правильный подход — один образ, разные entrypoint. + +**Порядок действий (решение):** + +``` +1. Обновить Dockerfile: + - Добавить COPY iot/ iot/ + - Добавить RUN go build -o iot-mqtt-bridge iot/cmd/mqtt-bridge/main.go + - Добавить COPY --from=builder /workspace/iot-mqtt-bridge . + +2. Обновить operator.yaml: тег v0.1.49 → v0.1.50 + +3. SSH: docker build + push v0.1.50 + +4. kubectl apply CRD IoTDevice (один раз, cluster-wide) + +5. kubectl apply EMQX (EMQX deployment + svc + configmap) + +6. kubectl apply operator v0.1.50 (подхватит IoT controller + IoT API) + +7. Bootstrap mqtt-bridge: + - Оператор должен быть живым (шаг 6) + - Создать IoTDevice "iot-bridge" через API → контроллер сгенерирует Secret в namespace sless-bridge + - Из Secret взять mqtt_username + mqtt_password + - kubectl create secret generic iot-bridge-credentials -n sless + - kubectl apply iot-mqtt-bridge.yaml + +8. Проверка end-to-end +``` + +**Риски и как их обходить:** + +- `sless-bridge` namespace может не существовать → создать заранее через kubectl +- EMQX может быть не готов к моменту запуска bridge → bridge сам делает retry (в коде есть reconnect loop) +- IoT API требует JWT-токен → при bootstrap curl с токеном из sless-operator-secret + +**Почему один образ для operator + bridge:** +Это не идеально с т.з. SRP, но практично: +- Не нужен отдельный CI pipeline +- Не нужен отдельный registry repo +- Bridge — простой процесс (~100 строк Go), не нагружает образ +- В будущем можно разделить, порог изменений низкий + +**Итог по мышлению:** Plan is solid. Начинаю выполнение. + +### Проблемы, найденные при выполнении (до → решение) + +**Проблема 1 — RBAC не настроен для iot.kube5s.ru:** +- Попытка создать IoTDevice через API → 403 Forbidden +- `sless-operator` ServiceAccount не имел прав на `iotdevices.iot.kube5s.ru` +- Причина: CRD для IoT — новая API-группа, в rbac.yaml её не было +- Решение: добавил в ClusterRole правила на `iot.kube5s.ru` (get/list/watch/create/update/patch/delete + status + finalizers) +- `kubectl apply -f rbac.yaml` → configured +- Вывод: при добавлении нового CRD API group ВСЕГДА нужно обновлять ClusterRole + +**Проблема 2 — EMQX 5.x требует обязательные поля node.cookie и node.data_dir:** +- EMQX CrashLoopBackOff с ошибкой: `required_field: node.cookie, node.data_dir` +- В нашем emqx.conf (HOCON) эти поля отсутствовали — думал что для single-node они необязательны +- На самом деле в EMQX 5.x они mandatory (в отличие от 4.x где были defaults) +- Решение: добавил `node {}` секцию: name=emqx@127.0.0.1, cookie=sless-emqx-cookie-mvp, data_dir=/opt/emqx/data +- kubectl apply обновил ConfigMap, rollout restart → EMQX поднялся +- Вывод: при обновлении ConfigMap Deployment не перезапускается автоматически — нужен `kubectl rollout restart` + +**Проблема 3 — kubectl logs берёт старый (crashing) pod:** +- deployment/emqx — логи шли со старого пода в CrashLoopBackOff +- Нужно указывать pod name явно для нового пода +- Это нормальное поведение kubectl — нет флага "новый pod" + +### Итоговый статус деплоя + +``` +emqx-6f9689fc99-4mbhr 1/1 Running ✅ +iot-mqtt-bridge-7d784d7d6b-n45fp 1/1 Running ✅ (3 restarts — reconnect loop до старта EMQX) +sless-operator-579dd6dcd5-fk2n8 1/1 Running ✅ +``` + +CRD применён: `iotdevices.iot.kube5s.ru created` +IoTDevice iot-bridge создан: phase=Active, credentials в secret iot-iot-bridge +Secret iot-bridge-credentials создан в namespace sless diff --git a/internal/api/handler/iot_device_handler.go b/internal/api/handler/iot_device_handler.go index 8ff107f..c6003ad 100644 --- a/internal/api/handler/iot_device_handler.go +++ b/internal/api/handler/iot_device_handler.go @@ -58,19 +58,19 @@ type iotDeviceUpdateRequest struct { // iotDeviceResponse — ответ при чтении одного IoTDevice. // MQTTPassword заполняется только из GetIoTDevice (чтение из Secret). type iotDeviceResponse struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - DeviceID string `json:"device_id"` - Enabled bool `json:"enabled"` - Phase iotv1alpha1.IoTDevicePhase `json:"phase"` - MQTTUsername string `json:"mqtt_username,omitempty"` - MQTTPassword string `json:"mqtt_password,omitempty"` // только в GET /devices/{name} - SecretName string `json:"secret_name,omitempty"` - TopicPrefix string `json:"topic_prefix,omitempty"` - LastConnected string `json:"last_connected,omitempty"` - Message string `json:"message,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - CreatedAt string `json:"created_at,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + DeviceID string `json:"device_id"` + Enabled bool `json:"enabled"` + Phase iotv1alpha1.IoTDevicePhase `json:"phase"` + MQTTUsername string `json:"mqtt_username,omitempty"` + MQTTPassword string `json:"mqtt_password,omitempty"` // только в GET /devices/{name} + SecretName string `json:"secret_name,omitempty"` + TopicPrefix string `json:"topic_prefix,omitempty"` + LastConnected string `json:"last_connected,omitempty"` + Message string `json:"message,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + CreatedAt string `json:"created_at,omitempty"` } // mqttAuthRequest — тело запроса от EMQX при MQTT CONNECT. @@ -126,11 +126,11 @@ func deviceToResponse(d *iotv1alpha1.IoTDevice, password string) iotDeviceRespon // НЕ защищён JWT middleware — доступен только из кластера (путь /internal/). // // Логика аутентификации: -// 1. Распарсить username → namespace + deviceId -// 2. Получить Secret iot-{deviceId} в namespace -// 3. Constant-time сравнение пароля (защита от timing attacks) -// 4. Проверить что IoTDevice существует и enabled=true -// 5. Обновить status.lastConnected в IoTDevice +// 1. Распарсить username → namespace + deviceId +// 2. Получить Secret iot-{deviceId} в namespace +// 3. Constant-time сравнение пароля (защита от timing attacks) +// 4. Проверить что IoTDevice существует и enabled=true +// 5. Обновить status.lastConnected в IoTDevice func (h *Handler) MQTTAuth(w http.ResponseWriter, r *http.Request) { var req mqttAuthRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/main.go b/main.go index dfcfd7c..70d45b1 100644 --- a/main.go +++ b/main.go @@ -27,8 +27,6 @@ import ( slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/controllers" - iotv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/api/v1alpha1" - iotcontrollers "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/controllers" slessapi "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/api" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/api/handler" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder" @@ -36,6 +34,8 @@ import ( "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/harbor" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3" + iotv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/api/v1alpha1" + iotcontrollers "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/controllers" //+kubebuilder:scaffold:imports )