ea330bd767
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).
164 lines
5.8 KiB
Go
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
|
|
}
|