afb4fe2631
BIND ignores an RRset-wide delete at a zone apex, so a DNSRecord for the apex NS can only add to what the zone was seeded with while reporting success. BindZone.spec.nameservers converges it per rdata.
124 lines
4.6 KiB
Go
124 lines
4.6 KiB
Go
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)
|
|
// CEL on the spec catches "@" and ""; a zone-qualified apex name reaches here.
|
|
apexNS := isApexNS(record.Spec.Name, record.Spec.Type, 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 && !apexNS {
|
|
_ = 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 apexNS {
|
|
return r.setPhase(ctx, &record, "Error", "ApexNSUnsupported",
|
|
"BIND ignores an RRset-wide delete at a zone apex, so this record can only append; publish the apex NS via BindZone.spec.nameservers")
|
|
}
|
|
|
|
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)
|
|
}
|