Files
bind-operator/internal/controller/helpers.go
T
unkinben ea330bd767
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Fix authoritative secondary replication (TSIG transfer + stable primary)
Secondaries never replicated any member zone: the master's catalog zone
requires key-authenticated AXFR (allow-transfer { key "transfer-key"; }),
but the rendered secondary config transferred without presenting the key,
so every catalog transfer was REFUSED and no member zones provisioned.
Two further gaps compounded it: member zones had no allow-transfer at all,
and secondaries pointed at the primary's pod IP, which dies on restart.

- Render the catalog transfer key into the secondary catalog-zones
  default-primaries and the secondary catalog zone primaries, so
  key-authenticated AXFR from the primary is accepted.
- Add allow-transfer { key "<transfer-key>"; } to catalog member primary
  zones (when the zone does not set an explicit allow-transfer), so
  secondaries can pull them; applied to existing zones via modzone.
- Point secondaries at the stable primary Service ClusterIP instead of the
  primary pod IP, so replication survives primary pod restarts (falls back
  to the pod IP when no primary Service exists).
2026-07-12 19:42:38 +10:00

164 lines
5.8 KiB
Go

package controller
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
"git.unkin.net/unkin/bind-operator/internal/bind"
)
const (
requeueShort = 15 * time.Second
requeueLong = 2 * time.Minute
managedByLabel = "app.kubernetes.io/managed-by"
managedByValue = "bind-operator"
clusterLabel = "bind.unkin.net/cluster"
finalizer = "bind.unkin.net/finalizer"
// defaultBindImage is used when BindCluster.spec.image is empty.
defaultBindImage = "internetsystemsconsortium/bind9:9.20"
)
func headlessServiceName(cluster string) string { return cluster + "-headless" }
func primaryServiceName(cluster string) string { return cluster + "-primary" }
// primaryPodSelector selects only the primary pod (ordinal 0) via the stable
// StatefulSet pod-name label, for the write Service.
func primaryPodSelector(cluster string) map[string]string {
s := commonLabels(cluster)
s["statefulset.kubernetes.io/pod-name"] = primaryPodName(cluster)
return s
}
func clientServiceName(cluster string) string { return cluster }
func primaryPodName(cluster string) string { return cluster + "-0" }
func configMapName(cluster string) string { return cluster + "-config" }
func keysSecretName(cluster string) string { return cluster + "-keys" }
func rndcSecretName(cluster string) string { return cluster + "-rndc" }
// primaryAddress is the in-cluster DNS name of the primary pod (ordinal 0).
func primaryAddress(cluster, namespace string) string {
return fmt.Sprintf("%s-0.%s.%s.svc.cluster.local", cluster, headlessServiceName(cluster), namespace)
}
// setReady sets the standard Ready condition on a status conditions slice.
func setReady(conds *[]metav1.Condition, gen int64, ok bool, reason, msg string) {
status := metav1.ConditionFalse
if ok {
status = metav1.ConditionTrue
}
meta.SetStatusCondition(conds, metav1.Condition{
Type: "Ready",
Status: status,
Reason: reason,
Message: msg,
ObservedGeneration: gen,
})
}
// commonLabels are applied to every object the operator creates for a cluster.
func commonLabels(cluster string) map[string]string {
return map[string]string{
managedByLabel: managedByValue,
clusterLabel: cluster,
}
}
// getCluster fetches the BindCluster referenced by clusterRef in namespace.
func getCluster(ctx context.Context, c client.Client, namespace, clusterRef string) (*bindv1alpha1.BindCluster, error) {
var cluster bindv1alpha1.BindCluster
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: clusterRef}, &cluster); err != nil {
return nil, err
}
return &cluster, nil
}
// primaryReady reports whether the primary pod of a cluster is Ready.
func primaryReady(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) bool {
var pod corev1.Pod
key := client.ObjectKey{Namespace: cluster.Namespace, Name: primaryPodName(cluster.Name)}
if err := c.Get(ctx, key, &pod); err != nil {
return false
}
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady {
return cond.Status == corev1.ConditionTrue
}
}
return false
}
// primaryPodIP returns the pod IP of a cluster's primary pod (ordinal 0), or an
// empty string if the pod has no IP yet. BIND's primaries/default-primaries
// only accept IP addresses (not hostnames), and zone seeding needs the address
// for glue, so the operator resolves the pod IP rather than using a DNS name.
func primaryPodIP(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) string {
var pod corev1.Pod
if err := c.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: primaryPodName(cluster.Name)}, &pod); err != nil {
return ""
}
return pod.Status.PodIP
}
// primaryTransferAddress returns the address secondaries use to reach the
// primary for catalog and zone AXFR. It prefers the primary Service ClusterIP,
// which is stable across primary pod restarts (the pod IP is not: it changes on
// every restart, leaving secondaries pointed at a dead address). It falls back
// to the primary pod IP when no primary Service is configured or its ClusterIP
// is not yet assigned.
func primaryTransferAddress(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) string {
if cluster.Spec.PrimaryService != nil {
var svc corev1.Service
if err := c.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: primaryServiceName(cluster.Name)}, &svc); err == nil {
if ip := svc.Spec.ClusterIP; ip != "" && ip != corev1.ClusterIPNone {
return ip
}
}
}
return primaryPodIP(ctx, c, cluster)
}
// resolveTSIG reads the material of a BindTSIGKey into TSIG credentials.
func resolveTSIG(ctx context.Context, c client.Client, namespace, keyRef string) (bind.TSIGCreds, error) {
var creds bind.TSIGCreds
if keyRef == "" {
return creds, fmt.Errorf("no TSIG key referenced")
}
var key bindv1alpha1.BindTSIGKey
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: keyRef}, &key); err != nil {
return creds, fmt.Errorf("get tsig key %s: %w", keyRef, err)
}
secretName := key.Status.SecretName
if secretName == "" {
secretName = key.Spec.SecretName
}
if secretName == "" {
secretName = keyRef + "-tsig"
}
var secret corev1.Secret
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretName}, &secret); err != nil {
return creds, fmt.Errorf("get tsig secret %s: %w", secretName, err)
}
keyName := key.Spec.KeyName
if keyName == "" {
keyName = keyRef
}
creds = bind.TSIGCreds{
Name: keyName,
Algorithm: string(secret.Data["algorithm"]),
Secret: string(secret.Data["secret"]),
}
if creds.Algorithm == "" {
creds.Algorithm = string(bindv1alpha1.TSIGHMACSHA256)
}
return creds, nil
}