Add a primary (write) Service routing to pod-0 #7

Merged
benvin merged 1 commits from benvin/primary-write-service into main 2026-07-04 22:37:40 +10:00
6 changed files with 148 additions and 9 deletions
+9 -1
View File
@@ -105,10 +105,18 @@ type BindClusterSpec struct {
// +optional
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
// Service controls how the cluster is exposed.
// Service controls how the cluster is exposed for reads (all pods).
// +optional
Service ClusterServiceSpec `json:"service,omitempty"`
// PrimaryService, when set, creates an additional Service routing only to the
// primary pod (ordinal 0) — the write endpoint for RFC2136/nsupdate, since
// secondaries reject updates. Reads still use Service (all pods). Use
// ClusterIP for in-cluster writers (e.g. external-dns) or LoadBalancer for
// external writers.
// +optional
PrimaryService *ClusterServiceSpec `json:"primaryService,omitempty"`
// NodeSelector for the BIND pods.
// +optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
+5
View File
@@ -301,6 +301,11 @@ func (in *BindClusterSpec) DeepCopyInto(out *BindClusterSpec) {
}
in.Resources.DeepCopyInto(&out.Resources)
in.Service.DeepCopyInto(&out.Service)
if in.PrimaryService != nil {
in, out := &in.PrimaryService, &out.PrimaryService
*out = new(ClusterServiceSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
@@ -1013,6 +1013,41 @@ spec:
type: string
description: NodeSelector for the BIND pods.
type: object
primaryService:
description: |-
PrimaryService, when set, creates an additional Service routing only to the
primary pod (ordinal 0) — the write endpoint for RFC2136/nsupdate, since
secondaries reject updates. Reads still use Service (all pods). Use
ClusterIP for in-cluster writers (e.g. external-dns) or LoadBalancer for
external writers.
properties:
annotations:
additionalProperties:
type: string
description: Annotations added to the client-facing Service (e.g.
PureLB/MetalLB hints).
type: object
externalTrafficPolicy:
description: |-
ExternalTrafficPolicy for a LoadBalancer/NodePort Service. Local preserves
client source IPs (required for source-IP ACLs on the DNS servers) but
only routes to nodes running a pod. Defaults to Cluster.
enum:
- Cluster
- Local
type: string
loadBalancerIP:
description: LoadBalancerIP requests a specific address when Type
is LoadBalancer.
type: string
type:
description: Type of the client-facing Service. Defaults to ClusterIP.
enum:
- ClusterIP
- LoadBalancer
- NodePort
type: string
type: object
recursion:
description: |-
Recursion overrides the default per-mode recursion setting. When nil,
@@ -1086,7 +1121,8 @@ spec:
type: object
type: object
service:
description: Service controls how the cluster is exposed.
description: Service controls how the cluster is exposed for reads
(all pods).
properties:
annotations:
additionalProperties:
+37 -1
View File
@@ -1318,6 +1318,41 @@ spec:
type: string
description: NodeSelector for the BIND pods.
type: object
primaryService:
description: |-
PrimaryService, when set, creates an additional Service routing only to the
primary pod (ordinal 0) — the write endpoint for RFC2136/nsupdate, since
secondaries reject updates. Reads still use Service (all pods). Use
ClusterIP for in-cluster writers (e.g. external-dns) or LoadBalancer for
external writers.
properties:
annotations:
additionalProperties:
type: string
description: Annotations added to the client-facing Service (e.g.
PureLB/MetalLB hints).
type: object
externalTrafficPolicy:
description: |-
ExternalTrafficPolicy for a LoadBalancer/NodePort Service. Local preserves
client source IPs (required for source-IP ACLs on the DNS servers) but
only routes to nodes running a pod. Defaults to Cluster.
enum:
- Cluster
- Local
type: string
loadBalancerIP:
description: LoadBalancerIP requests a specific address when Type
is LoadBalancer.
type: string
type:
description: Type of the client-facing Service. Defaults to ClusterIP.
enum:
- ClusterIP
- LoadBalancer
- NodePort
type: string
type: object
recursion:
description: |-
Recursion overrides the default per-mode recursion setting. When nil,
@@ -1391,7 +1426,8 @@ spec:
type: object
type: object
service:
description: Service controls how the cluster is exposed.
description: Service controls how the cluster is exposed for reads
(all pods).
properties:
annotations:
additionalProperties:
+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 {