Add a primary (write) Service routing to pod-0
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

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
This commit is contained in:
2026-07-04 22:29:55 +10:00
parent e0bd3973ed
commit 28ae6538cb
6 changed files with 148 additions and 9 deletions
+46 -1
View File
@@ -277,7 +277,52 @@ func (r *BindClusterReconciler) reconcileServices(ctx context.Context, c *bindv1
if svcType == corev1.ServiceTypeLoadBalancer || svcType == corev1.ServiceTypeNodePort {
client.Spec.ExternalTrafficPolicy = c.Spec.Service.ExternalTrafficPolicy
}
return r.upsertService(ctx, c, client)
if err := r.upsertService(ctx, c, client); err != nil {
return err
}
// Primary (write) Service: routes only to pod-0. Created when configured,
// deleted when removed.
return r.reconcilePrimaryService(ctx, c, dnsPorts)
}
func (r *BindClusterReconciler) reconcilePrimaryService(ctx context.Context, c *bindv1alpha1.BindCluster, dnsPorts []corev1.ServicePort) error {
name := primaryServiceName(c.Name)
if c.Spec.PrimaryService == nil {
var existing corev1.Service
err := r.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: name}, &existing)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
return client.IgnoreNotFound(r.Delete(ctx, &existing))
}
ps := c.Spec.PrimaryService
psType := ps.Type
if psType == "" {
psType = corev1.ServiceTypeClusterIP
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: c.Namespace,
Labels: commonLabels(c.Name),
Annotations: ps.Annotations,
},
Spec: corev1.ServiceSpec{
Type: psType,
Selector: primaryPodSelector(c.Name),
Ports: dnsPorts,
LoadBalancerIP: ps.LoadBalancerIP,
},
}
if psType == corev1.ServiceTypeLoadBalancer || psType == corev1.ServiceTypeNodePort {
svc.Spec.ExternalTrafficPolicy = ps.ExternalTrafficPolicy
}
return r.upsertService(ctx, c, svc)
}
func (r *BindClusterReconciler) reconcileStatefulSet(ctx context.Context, c *bindv1alpha1.BindCluster) (*appsv1.StatefulSet, error) {
+14 -5
View File
@@ -29,11 +29,20 @@ const (
)
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" }
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 {