Initial bind-operator: 9 CRDs + controllers
Implements a Kubernetes operator that manages fleets of BIND9 servers declaratively, using controller-runtime (matching forgebot conventions). - add BindCluster reconciler: StatefulSet (pod-0 primary, secondaries), headless + client Services, rendered named.conf ConfigMap, TSIG keys Secret and rndc control Secret; watches dependent CRs to re-render - add BindTSIGKey reconciler that generates key material into a Secret - add BindZone/DNSRecord reconcilers using fully-dynamic delivery (rndc addzone + TSIG nsupdate against the primary pod) - add BindCatalogZone reconciler so secondaries auto-provision zones - add BindPolicy (RPZ), BindDNSSECPolicy, BindView, BindACL reconcilers - render primary/secondary named.conf variants selected by pod ordinal - generate CRDs, deepcopy and RBAC; add samples mapping the three Puppet roles (authoritative/resolver/external-dns) to three BindClusters - add Makefile, Dockerfile.operator, Woodpecker CI and kind manifests
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user