Files
bind-operator/internal/controller/helpers.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

137 lines
4.6 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 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
}
// 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
}