Компоненты: - iot-operator: controller-manager (IoTDevice CRD) + REST API (порт 9090) - mqtt-bridge: MQTT (EMQX) → Kafka bridge - kafka-consumer: Kafka → Postgres pipeline Модуль: gitea.services.ngcloud.ru/Nail/IoT Все 3 бинарника собираются, import paths адаптированы.
176 lines
7.7 KiB
Go
176 lines
7.7 KiB
Go
// Создано: 2026-04-04
|
||
// iotdevice_controller.go — контроллер IoTDevice CRD.
|
||
// Логика Reconcile:
|
||
// 1. Добавить finalizer при первом создании
|
||
// 2. При удалении (DeletionTimestamp != nil) — убрать finalizer.
|
||
// Secret удалится автоматически по OwnerReference (cascade GC).
|
||
// 3. Если spec.enabled=false — установить phase=Disabled, не создавать Secret.
|
||
// Если Secret уже существует — НЕ удалять (при re-enable пароль сохранится).
|
||
// 4. Если Secret не существует — сгенерировать пароль, создать Secret с OwnerRef.
|
||
// 5. Обновить status: phase, mqttUsername, secretName, topicPrefix.
|
||
|
||
package controllers
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
|
||
corev1 "k8s.io/api/core/v1"
|
||
"k8s.io/apimachinery/pkg/api/errors"
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||
"k8s.io/apimachinery/pkg/runtime"
|
||
ctrl "sigs.k8s.io/controller-runtime"
|
||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||
|
||
iotv1alpha1 "gitea.services.ngcloud.ru/Nail/IoT/api/v1alpha1"
|
||
)
|
||
|
||
// iotDeviceFinalizer — имя finalizer для IoTDevice.
|
||
// Гарантирует что контроллер обработает удаление до того как k8s удалит объект.
|
||
const iotDeviceFinalizer = "iot.kube5s.ru/device-cleanup"
|
||
|
||
// IoTDeviceReconciler управляет жизненным циклом IoTDevice CRD.
|
||
type IoTDeviceReconciler struct {
|
||
client.Client
|
||
Scheme *runtime.Scheme
|
||
}
|
||
|
||
// +kubebuilder:rbac:groups=iot.kube5s.ru,resources=iotdevices,verbs=get;list;watch;create;update;patch;delete
|
||
// +kubebuilder:rbac:groups=iot.kube5s.ru,resources=iotdevices/status,verbs=get;update;patch
|
||
// +kubebuilder:rbac:groups=iot.kube5s.ru,resources=iotdevices/finalizers,verbs=update
|
||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;delete
|
||
|
||
// Reconcile — главный цикл управления IoTDevice.
|
||
func (r *IoTDeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||
logger := log.FromContext(ctx)
|
||
|
||
// Читаем IoTDevice из k8s
|
||
device := &iotv1alpha1.IoTDevice{}
|
||
if err := r.Get(ctx, req.NamespacedName, device); err != nil {
|
||
if errors.IsNotFound(err) {
|
||
// Объект удалён — финализация уже завершена
|
||
return ctrl.Result{}, nil
|
||
}
|
||
return ctrl.Result{}, fmt.Errorf("get iotdevice: %w", err)
|
||
}
|
||
|
||
// Если объект помечен к удалению — убираем finalizer.
|
||
// Secret удалится каскадно через OwnerReference — явно удалять не нужно.
|
||
if !device.DeletionTimestamp.IsZero() {
|
||
if controllerutil.ContainsFinalizer(device, iotDeviceFinalizer) {
|
||
controllerutil.RemoveFinalizer(device, iotDeviceFinalizer)
|
||
if err := r.Update(ctx, device); err != nil {
|
||
return ctrl.Result{}, fmt.Errorf("remove finalizer: %w", err)
|
||
}
|
||
logger.Info("removed finalizer from IoTDevice", "device", req.Name)
|
||
}
|
||
return ctrl.Result{}, nil
|
||
}
|
||
|
||
// Добавляем finalizer при первом создании объекта
|
||
if !controllerutil.ContainsFinalizer(device, iotDeviceFinalizer) {
|
||
controllerutil.AddFinalizer(device, iotDeviceFinalizer)
|
||
if err := r.Update(ctx, device); err != nil {
|
||
return ctrl.Result{}, fmt.Errorf("add finalizer: %w", err)
|
||
}
|
||
// Requeue — обязательно, чтобы продолжить reconcile с обновлённым объектом
|
||
return ctrl.Result{Requeue: true}, nil
|
||
}
|
||
|
||
// Если устройство отключено — обновляем статус, не создаём Secret
|
||
if !device.Spec.Enabled {
|
||
return ctrl.Result{}, r.setStatus(ctx, device, iotv1alpha1.IoTDevicePhaseDisabled, "Device is disabled")
|
||
}
|
||
|
||
// Имя Secret и MQTT username по формату из плана
|
||
secretName := "iot-" + device.Spec.DeviceID
|
||
mqttUsername := fmt.Sprintf("%s_%s", device.Namespace, device.Spec.DeviceID)
|
||
|
||
// Проверяем наличие Secret — если нет, создаём с новыми credentials
|
||
existingSecret := &corev1.Secret{}
|
||
err := r.Get(ctx, client.ObjectKey{Namespace: device.Namespace, Name: secretName}, existingSecret)
|
||
if errors.IsNotFound(err) {
|
||
// Генерируем криптографически стойкий пароль: 32 байта = 64 hex символа
|
||
password, genErr := generateMQTTPassword()
|
||
if genErr != nil {
|
||
setErr := r.setStatus(ctx, device, iotv1alpha1.IoTDevicePhaseError, fmt.Sprintf("generate password: %v", genErr))
|
||
return ctrl.Result{}, setErr
|
||
}
|
||
|
||
secret := &corev1.Secret{
|
||
ObjectMeta: metav1.ObjectMeta{
|
||
Name: secretName,
|
||
Namespace: device.Namespace,
|
||
},
|
||
Data: map[string][]byte{
|
||
"mqtt-username": []byte(mqttUsername),
|
||
"mqtt-password": []byte(password),
|
||
},
|
||
}
|
||
|
||
// OwnerReference — Secret принадлежит IoTDevice.
|
||
// При удалении IoTDevice k8s каскадно удалит Secret.
|
||
if ownerErr := controllerutil.SetControllerReference(device, secret, r.Scheme); ownerErr != nil {
|
||
return ctrl.Result{}, fmt.Errorf("set owner reference: %w", ownerErr)
|
||
}
|
||
|
||
if createErr := r.Create(ctx, secret); createErr != nil {
|
||
setErr := r.setStatus(ctx, device, iotv1alpha1.IoTDevicePhaseError, fmt.Sprintf("create secret: %v", createErr))
|
||
return ctrl.Result{}, setErr
|
||
}
|
||
logger.Info("created MQTT credentials Secret", "secret", secretName, "device", device.Spec.DeviceID)
|
||
|
||
} else if err != nil {
|
||
// Неожиданная ошибка при чтении Secret
|
||
return ctrl.Result{}, fmt.Errorf("get secret: %w", err)
|
||
}
|
||
// else — Secret уже существует, ничего не делаем (пароль не меняем)
|
||
|
||
// Обновляем status — отражаем актуальное состояние устройства
|
||
device.Status.Phase = iotv1alpha1.IoTDevicePhaseActive
|
||
device.Status.MQTTUsername = mqttUsername
|
||
device.Status.SecretName = secretName
|
||
device.Status.TopicPrefix = device.Namespace + "/"
|
||
device.Status.Message = ""
|
||
|
||
if statusErr := r.Status().Update(ctx, device); statusErr != nil {
|
||
return ctrl.Result{}, fmt.Errorf("update status: %w", statusErr)
|
||
}
|
||
|
||
return ctrl.Result{}, nil
|
||
}
|
||
|
||
// setStatus — вспомогательная функция для обновления status.Phase и status.Message.
|
||
// Используется для промежуточных состояний (Disabled, Error).
|
||
func (r *IoTDeviceReconciler) setStatus(ctx context.Context, device *iotv1alpha1.IoTDevice, phase iotv1alpha1.IoTDevicePhase, message string) error {
|
||
device.Status.Phase = phase
|
||
device.Status.Message = message
|
||
if err := r.Status().Update(ctx, device); err != nil {
|
||
return fmt.Errorf("update status to %s: %w", phase, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// generateMQTTPassword генерирует криптографически стойкий пароль для MQTT.
|
||
// 32 байта случайных данных → hex строка 64 символа = 256 бит энтропии.
|
||
func generateMQTTPassword() (string, error) {
|
||
buf := make([]byte, 32)
|
||
if _, err := rand.Read(buf); err != nil {
|
||
return "", fmt.Errorf("rand.Read: %w", err)
|
||
}
|
||
return hex.EncodeToString(buf), nil
|
||
}
|
||
|
||
// SetupWithManager регистрирует контроллер в manager'е.
|
||
// Watches только IoTDevice — secrets обновляются через reconcile loop при необходимости.
|
||
func (r *IoTDeviceReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||
return ctrl.NewControllerManagedBy(mgr).
|
||
For(&iotv1alpha1.IoTDevice{}).
|
||
Owns(&corev1.Secret{}).
|
||
Complete(r)
|
||
}
|