Files
bind-operator/internal/controller/bindcatalogzone_controller.go
T
unkinben fb103a9e95
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Fix zone provisioning: seed glue + IP primaries
Two bugs made every provisioned zone fail to load:

1. The seed zone's apex NS (ns1.<zone>) is in-zone but had no address
   record, so BIND check-integrity refused to load it and rndc addzone
   reverted. Add a glue A record pointing at the primary pod IP.
2. Secondaries rendered primaries/default-primaries with the primary's
   DNS name, but BIND only accepts IP addresses there (it read the name
   as a remote-servers list and failed config load, crash-looping the
   secondary). Render the primary pod IP instead, and watch Pods so the
   config re-renders when that IP appears or changes.

- bind.WriteSeedZone writes 'ns1 IN A <primaryIP>' glue
- controllers resolve primaryPodIP and pass it to the seed (requeue if
  the primary has no IP yet)
- BindCluster renders PrimaryAddress from pod-0's IP and watches Pods
- render omits catalog primaries when the IP is unknown (no empty list)
2026-07-03 21:33:31 +10:00

116 lines
4.4 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/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, "") {
primaryIP := primaryPodIP(ctx, r.Client, cluster)
if primaryIP == "" {
return r.fail(ctx, &catalog, "PrimaryNoIP", "waiting for primary pod IP")
}
if err := r.Exec.WriteSeedZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, bind.CatalogFilePath(catalog.Spec.ZoneName), primaryIP, 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)
}