// Создано: 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) }