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,111 @@
|
||||
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/log"
|
||||
|
||||
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||
)
|
||||
|
||||
// BindCatalogZoneReconciler creates and maintains the catalog zone on a cluster
|
||||
// primary so secondaries auto-provision member zones.
|
||||
type BindCatalogZoneReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
Exec *bind.Executor
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindcatalogzones,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindcatalogzones/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones;bindtsigkeys,verbs=get;list;watch
|
||||
|
||||
func (r *BindCatalogZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var catalog bindv1alpha1.BindCatalogZone
|
||||
if err := r.Get(ctx, req.NamespacedName, &catalog); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
cluster, err := getCluster(ctx, r.Client, catalog.Namespace, catalog.Spec.ClusterRef)
|
||||
if err != nil {
|
||||
return r.fail(ctx, &catalog, "ClusterMissing", err.Error())
|
||||
}
|
||||
primaryPod := primaryPodName(cluster.Name)
|
||||
|
||||
if !primaryReady(ctx, r.Client, cluster) || r.Exec == nil {
|
||||
return r.fail(ctx, &catalog, "PrimaryNotReady", "waiting for cluster primary")
|
||||
}
|
||||
|
||||
creds, err := resolveTSIG(ctx, r.Client, catalog.Namespace, catalog.Spec.TransferKeyRef)
|
||||
if err != nil {
|
||||
return r.fail(ctx, &catalog, "NoTransferKey", err.Error())
|
||||
}
|
||||
|
||||
// Ensure the catalog zone exists on the primary.
|
||||
if !r.Exec.ZoneExists(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, "") {
|
||||
if err := r.Exec.WriteSeedZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, bind.CatalogFilePath(catalog.Spec.ZoneName), "", 1); err != nil {
|
||||
return r.fail(ctx, &catalog, "SeedFailed", err.Error())
|
||||
}
|
||||
}
|
||||
zoneConfig := fmt.Sprintf("{ type primary; file \"%s\"; allow-transfer { key \"%s\"; }; allow-update { key \"%s\"; }; };",
|
||||
bind.CatalogFilePath(catalog.Spec.ZoneName), catalog.Spec.TransferKeyRef, catalog.Spec.TransferKeyRef)
|
||||
if err := r.Exec.AddZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, "", zoneConfig); err != nil {
|
||||
return r.fail(ctx, &catalog, "AddZoneFailed", err.Error())
|
||||
}
|
||||
|
||||
// Catalog zones must advertise their schema version (RFC 9432: "2").
|
||||
versionUpdate := bind.RecordUpdate{
|
||||
FQDN: "version." + catalog.Spec.ZoneName + ".",
|
||||
Type: "TXT",
|
||||
TTL: 3600,
|
||||
Values: []string{"\"2\""},
|
||||
}
|
||||
if err := r.Exec.NSUpdate(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, creds, []bind.RecordUpdate{versionUpdate}); err != nil {
|
||||
return r.fail(ctx, &catalog, "VersionUpdateFailed", err.Error())
|
||||
}
|
||||
|
||||
// Count member zones for status.
|
||||
var zones bindv1alpha1.BindZoneList
|
||||
members := int32(0)
|
||||
if err := r.List(ctx, &zones, client.InNamespace(catalog.Namespace)); err == nil {
|
||||
for i := range zones.Items {
|
||||
z := &zones.Items[i]
|
||||
if z.Spec.ClusterRef == cluster.Name && catalogEnabled(z) {
|
||||
members++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catalog.Status.Ready = true
|
||||
catalog.Status.MemberCount = members
|
||||
catalog.Status.ObservedGeneration = catalog.Generation
|
||||
setReady(&catalog.Status.Conditions, catalog.Generation, true, "Ready", "catalog zone provisioned")
|
||||
if err := r.Status().Update(ctx, &catalog); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("catalog zone reconciled", "zone", catalog.Spec.ZoneName, "members", members)
|
||||
return ctrl.Result{RequeueAfter: requeueLong}, nil
|
||||
}
|
||||
|
||||
func (r *BindCatalogZoneReconciler) fail(ctx context.Context, catalog *bindv1alpha1.BindCatalogZone, reason, msg string) (ctrl.Result, error) {
|
||||
catalog.Status.Ready = false
|
||||
catalog.Status.ObservedGeneration = catalog.Generation
|
||||
setReady(&catalog.Status.Conditions, catalog.Generation, false, reason, msg)
|
||||
if err := r.Status().Update(ctx, catalog); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||
}
|
||||
|
||||
func (r *BindCatalogZoneReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&bindv1alpha1.BindCatalogZone{}).
|
||||
Complete(r)
|
||||
}
|
||||
Reference in New Issue
Block a user