feat(iot): Этап 1 — CRD IoTDevice + контроллер credentials

- iot/api/v1alpha1/device_types.go — CRD IoTDevice
- iot/api/v1alpha1/groupversion_info.go — API group iot.kube5s.ru/v1alpha1
- iot/api/v1alpha1/zz_generated.deepcopy.go — deepcopy (controller-gen)
- iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml — CRD манифест
- iot/controllers/iotdevice_controller.go — Reconcile: Secret с MQTT credentials
- main.go — регистрация IoT схемы и IoTDeviceReconciler

go build ./... — ошибок нет
This commit is contained in:
Naeel
2026-04-04 09:32:47 +03:00
parent 6dc2dc69ba
commit 716efafda8
7 changed files with 670 additions and 0 deletions
+122
View File
@@ -75,6 +75,128 @@ IoT Device → MQTT (topic: {user-prefix}/device/telemetry)
---
## Агент: GitHub Copilot (Claude Sonnet 4.6)
## Дата сессии: 2026-04-04
### Задача: Реализация IoT MVP по плану из doc/iot-mvp-plan.md
---
### Шаг 0: Изучение кодовой базы (перед написанием кода)
Прочитал следующие файлы:
- `api/v1alpha1/function_types.go` — паттерн написания CRD типов
- `api/v1alpha1/trigger_types.go` — использование TriggerType string const
- `api/v1alpha1/groupversion_info.go` — паттерн регистрации API group
- `controllers/function_controller.go` — паттерн контроллера, finalizer, Reconcile
- `controllers/service_controller.go` — второй контроллер для сравнения
- `internal/api/router.go` — регистрация маршрутов, auth middleware
- `main.go` — как регистрируются контроллеры, как подключается API
- `go.mod` — версии зависимостей (k8s v0.26.0, controller-runtime v0.14.1)
#### Что понял
**Go module**: `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless`
**API сервер**: gorilla/mux, auth через middleware на `/v1/` subrouter
**Паттерн контроллера**: `r.Get → finalizer → business logic → r.Status().Update()`
**Finalizer**: у function = `sless.kube5s.ru/finalizer`, у service = `sless.kube5s.ru/service-finalizer`
- Используют ручной containsString/removeString, **НЕ** controllerutil
- Я решил использовать `controllerutil.ContainsFinalizer/AddFinalizer/RemoveFinalizer` — это более современный и рекомендуемый подход из controller-runtime
**OwnerReference**: нигде не вижу в существующих контроллерах. Для IoT Secret нужна — чтобы Secret автоматически удалялся при удалении IoTDevice через cascade deletion. Использую `controllerutil.SetControllerReference`.
#### Почему controllerutil вместо ручного containsString
Существующий код использует устаревший подход (хелперы `containsString`/`removeString`). `controllerutil` — официальный API controller-runtime для работы с finalizers. Это не рефакторинг старого кода — это новый контроллер, пишу его правильно сразу.
---
### Шаг 1: CRD файлы и контроллер
#### Решение по структуре директорий
По плану: `iot/api/v1alpha1/` и `iot/controllers/`. Это отдельный package path от `api/v1alpha1`.
- IoT CRD package: `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/api/v1alpha1`
- IoT контроллер package: `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/iot/controllers`
- Тот же go.mod — не нужно создавать отдельный модуль
#### Решение по API Group
По плану: `iot.kube5s.ru` — отдельная от `sless.kube5s.ru`.
Причина: при выносе в отдельную репу CRD не будет конфликтовать. Принимаю.
#### MQTTUsername формат
По плану: `{namespace}_{deviceId}`.
Пример: `sless-abc123def456_sensor-01`
Причина: EMQX требует глобально уникальный username. Namespace даёт изоляцию.
#### Secret name
По плану: `iot-{deviceId}`.
**Возможная проблема**: deviceId может содержать символы недопустимые в k8s Secret именах (только [a-z0-9-]).
**Решение**: в kubebuilder validation на DeviceID добавить regex `[a-z0-9-]+`. Если deviceId уже проходит валидацию — проблемы нет.
В плане валидация не упомянута, но это необходимо чтобы имя Secret было валидным. Добавлю `+kubebuilder:validation:Pattern`.
#### Генерация пароля
32 байта через `crypto/rand.Read``hex.EncodeToString` = 64 символа.
Это достаточно энтропии (256 бит).
#### OwnerReference у Secret
С OwnerReference Secret автоматически удалится при удалении IoTDevice (cascade GC в k8s).
Поэтому в finalizer обработчике нет нужды явно удалять Secret — просто убираем finalizer.
**Но есть нюанс**: если IoTDevice и Secret находятся в одном namespace — cascade deletion работает.
В нашем случае оба в `sless-{hash}` — OK.
#### Обработка статуса
`r.Status().Update()` — только subresource. Не трогает `spec` или `metadata`. Это важно чтобы не вызвать лишний reconcile цикл (обновление spec → новый reconcile → loop).
#### Disabled устройство
Если `spec.enabled == false`:
- Secret НЕ создаём (устройство не должно подключаться)
- Если Secret уже существует — НЕ удаляем (при re-enable пароль не изменится)
- Status: phase = "Disabled"
Это соответствует плану.
**Стоп — перечитал план**: "Установить status.phase = 'Disabled' — НЕ удалять Secret". Значит если disabled — просто обновить статус, Secret остаётся. Принимаю.
---
### Что создаю (Этап 1)
1. `iot/api/v1alpha1/device_types.go` — CRD IoTDevice
2. `iot/api/v1alpha1/groupversion_info.go` — API group iot.kube5s.ru/v1alpha1
3. `iot/controllers/iotdevice_controller.go` — контроллер
4. `iot/config/crd/bases/` — директория для CRD YAML (создаётся controller-gen через SSH)
5. Обновление `main.go` — регистрация IoT схемы и контроллера
Этапы 2+ (MQTT auth, EMQX, API routes, Terraform) — отдельно после одобрения Этапа 1.
---
### Результат выполнения Этапа 1
**Создано:**
- `iot/api/v1alpha1/device_types.go` — CRD IoTDevice с IoTDevicePhase константами
- `iot/api/v1alpha1/groupversion_info.go` — API group `iot.kube5s.ru/v1alpha1`
- `iot/api/v1alpha1/zz_generated.deepcopy.go` — сгенерирован controller-gen
- `iot/config/crd/bases/iot.kube5s.ru_iotdevices.yaml` — CRD манифест
- `iot/controllers/iotdevice_controller.go` — контроллер с полным Reconcile loop
- `main.go` — добавлена регистрация IoT схемы и контроллера
**Проверка:** `go build ./...` — ошибок нет.
**Отклонения от плана (с обоснованием):**
- Добавлен `+kubebuilder:validation:Pattern` на `DeviceID` — необходимо чтобы имя Secret `iot-{deviceId}` было валидным k8s именем. В плане не указано, но без валидации контроллер может сломаться.
- Использован `controllerutil.ContainsFinalizer/AddFinalizer/RemoveFinalizer` вместо ручных хелперов — более современный подход из controller-runtime. Старый код не трогал.
- Добавлена константа `IoTDevicePhase` (типизированный string) вместо plain string в плане — для type-safety и читаемости.
---
## Задача: Решения по архитектуре IoT MVP
### Вопрос 1: Репозиторий — та же или новая?