package controller import ( "context" "fmt" "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" bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1" "git.unkin.net/unkin/bind-operator/internal/bind" ) // DNSRecordReconciler applies individual record sets to a zone via TSIG dynamic // update — the external-dns write path as a CRD. type DNSRecordReconciler struct { client.Client Scheme *runtime.Scheme Exec *bind.Executor } // +kubebuilder:rbac:groups=bind.unkin.net,resources=dnsrecords,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=bind.unkin.net,resources=dnsrecords/status,verbs=get;update;patch // +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones;bindtsigkeys,verbs=get;list;watch func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) var record bindv1alpha1.DNSRecord if err := r.Get(ctx, req.NamespacedName, &record); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } var zone bindv1alpha1.BindZone if err := r.Get(ctx, client.ObjectKey{Namespace: record.Namespace, Name: record.Spec.ZoneRef}, &zone); err != nil { return r.setPhase(ctx, &record, "Error", "ZoneMissing", err.Error()) } cluster, err := getCluster(ctx, r.Client, record.Namespace, zone.Spec.ClusterRef) if err != nil { return r.setPhase(ctx, &record, "Error", "ClusterMissing", err.Error()) } primaryPod := primaryPodName(cluster.Name) name := fqdn(record.Spec.Name, zone.Spec.ZoneName) creds, err := resolveTSIG(ctx, r.Client, record.Namespace, zone.Spec.UpdateKeyRef) if err != nil { return r.setPhase(ctx, &record, "Error", "NoUpdateKey", fmt.Sprintf("zone %s: %v", zone.Name, err)) } // Deletion via finalizer: remove the RRset. if !record.DeletionTimestamp.IsZero() { if controllerutil.ContainsFinalizer(&record, finalizer) { if primaryReady(ctx, r.Client, cluster) && r.Exec != nil { _ = r.Exec.NSUpdate(ctx, record.Namespace, primaryPod, zone.Spec.ZoneName, creds, []bind.RecordUpdate{{FQDN: name, Type: record.Spec.Type, Delete: true}}) } controllerutil.RemoveFinalizer(&record, finalizer) if err := r.Update(ctx, &record); err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil } if !controllerutil.ContainsFinalizer(&record, finalizer) { controllerutil.AddFinalizer(&record, finalizer) if err := r.Update(ctx, &record); err != nil { return ctrl.Result{}, err } } if !primaryReady(ctx, r.Client, cluster) || r.Exec == nil { return r.setPhase(ctx, &record, "Pending", "PrimaryNotReady", "waiting for cluster primary") } ttl := zone.Spec.DefaultTTL if record.Spec.TTL != nil { ttl = *record.Spec.TTL } update := bind.RecordUpdate{FQDN: name, Type: record.Spec.Type, TTL: ttl, Values: record.Spec.Values} if err := r.Exec.NSUpdate(ctx, record.Namespace, primaryPod, zone.Spec.ZoneName, creds, []bind.RecordUpdate{update}); err != nil { return r.setPhase(ctx, &record, "Error", "UpdateFailed", err.Error()) } record.Status.FQDN = name record.Status.Phase = "Applied" record.Status.ObservedGeneration = record.Generation setReady(&record.Status.Conditions, record.Generation, true, "Applied", "record applied via dynamic update") if err := r.Status().Update(ctx, &record); err != nil { return ctrl.Result{}, err } logger.Info("record applied", "record", name, "type", record.Spec.Type) return ctrl.Result{}, nil } func (r *DNSRecordReconciler) setPhase(ctx context.Context, record *bindv1alpha1.DNSRecord, phase, reason, msg string) (ctrl.Result, error) { record.Status.Phase = phase record.Status.ObservedGeneration = record.Generation setReady(&record.Status.Conditions, record.Generation, phase == "Applied", reason, msg) if err := r.Status().Update(ctx, record); err != nil { return ctrl.Result{}, err } if phase == "Error" || phase == "Pending" { return ctrl.Result{RequeueAfter: requeueShort}, nil } return ctrl.Result{}, nil } func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&bindv1alpha1.DNSRecord{}). Complete(r) }