28ae6538cb
Secondaries reject RFC2136/nsupdate writes, but the read Service round- robins across all pods. Add an optional per-cluster write endpoint that targets only the primary pod (ordinal 0) via the StatefulSet pod-name label. Reads keep using the all-pods Service. - api: BindCluster.spec.primaryService (*ClusterServiceSpec) — ClusterIP for in-cluster writers (external-dns) or LoadBalancer for external - reconcilePrimaryService creates <cluster>-primary selecting pod-0 when set, deletes it when unset - regenerate CRDs + install.yaml
146 lines
5.0 KiB
Go
146 lines
5.0 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
|
|
}
|
|
|
|
// 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
|
|
}
|