d5e08607c1
Dynamically-updated primary zones were only reaching the secondary pods on the hardcoded 1h SOA refresh: the operator emitted no NOTIFY, and a zone's only apex NS is the primary itself, so default 'notify yes' reached no one. Queries load-balanced across the serve VIP hit stale secondaries and returned NXDOMAIN (negatively cached downstream for the 300s SOA minimum), so records flapped for up to an hour after every update. Add 'notify explicit' + 'also-notify' with the secondary pod IPs to primary zone stanzas so an update NOTIFYs the secondaries for an immediate IXFR. Applied via modzone, so existing zones pick it up on the next reconcile. Also shorten the seed SOA refresh/retry/minimum as a fallback for missed NOTIFYs and to shrink stale-NXDOMAIN negative caching.
193 lines
7.0 KiB
Go
193 lines
7.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"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)
|
|
}
|
|
|
|
// secondaryPodIPs returns the pod IPs of a cluster's secondary pods (every pod
|
|
// except the ordinal-0 primary) that currently have an address. The primary
|
|
// uses this list as its zone `also-notify` set, so a change to a primary zone
|
|
// (in particular a dynamic update) triggers an immediate NOTIFY -> IXFR to the
|
|
// secondaries instead of leaving them stale until the next SOA refresh. The
|
|
// list is sorted so the rendered zone config is stable and does not churn
|
|
// modzone on every reconcile. Pod IPs change across restarts, so the caller
|
|
// relies on the zone controller's periodic requeue to refresh the set (a
|
|
// restarted secondary re-transfers the whole zone on load regardless). Returns
|
|
// nil for a single-replica cluster.
|
|
func secondaryPodIPs(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) []string {
|
|
var pods corev1.PodList
|
|
if err := c.List(ctx, &pods, client.InNamespace(cluster.Namespace), client.MatchingLabels(commonLabels(cluster.Name))); err != nil {
|
|
return nil
|
|
}
|
|
primary := primaryPodName(cluster.Name)
|
|
var ips []string
|
|
for i := range pods.Items {
|
|
p := &pods.Items[i]
|
|
if p.Name == primary || p.Status.PodIP == "" {
|
|
continue
|
|
}
|
|
ips = append(ips, p.Status.PodIP)
|
|
}
|
|
sort.Strings(ips)
|
|
return ips
|
|
}
|
|
|
|
// 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
|
|
}
|