Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49df29a072 | |||
| ea3d71fa93 | |||
| 28ae6538cb | |||
| e0bd3973ed | |||
| 547d168c12 | |||
| 9bc4436c79 | |||
| f0e851c0bc | |||
| 55e80b467c | |||
| cd25c94efc | |||
| fb103a9e95 |
@@ -33,6 +33,13 @@ type ClusterServiceSpec struct {
|
||||
// +optional
|
||||
LoadBalancerIP string `json:"loadBalancerIP,omitempty"`
|
||||
|
||||
// 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.
|
||||
// +kubebuilder:validation:Enum=Cluster;Local
|
||||
// +optional
|
||||
ExternalTrafficPolicy corev1.ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty"`
|
||||
|
||||
// Annotations added to the client-facing Service (e.g. PureLB/MetalLB hints).
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
@@ -98,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"`
|
||||
|
||||
@@ -16,6 +16,12 @@ const (
|
||||
// BindTSIGKeySpec defines a TSIG key. If no existing key material is imported,
|
||||
// the operator generates a random key and stores it in a Secret.
|
||||
type BindTSIGKeySpec struct {
|
||||
// ClusterRef names the BindCluster this key is included in. When empty the
|
||||
// key is shared with every cluster in the namespace (useful when multiple
|
||||
// clusters share one namespace).
|
||||
// +optional
|
||||
ClusterRef string `json:"clusterRef,omitempty"`
|
||||
|
||||
// Algorithm is the HMAC algorithm. Defaults to hmac-sha256.
|
||||
// +kubebuilder:default="hmac-sha256"
|
||||
// +optional
|
||||
|
||||
@@ -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:
|
||||
@@ -1094,6 +1130,15 @@ spec:
|
||||
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.
|
||||
|
||||
@@ -66,6 +66,12 @@ spec:
|
||||
- hmac-sha1
|
||||
- hmac-md5
|
||||
type: string
|
||||
clusterRef:
|
||||
description: |-
|
||||
ClusterRef names the BindCluster this key is included in. When empty the
|
||||
key is shared with every cluster in the namespace (useful when multiple
|
||||
clusters share one namespace).
|
||||
type: string
|
||||
importExisting:
|
||||
description: |-
|
||||
ImportExisting, when true, means the referenced Secret already contains a
|
||||
|
||||
+52
-1
@@ -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:
|
||||
@@ -1399,6 +1435,15 @@ spec:
|
||||
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.
|
||||
@@ -2043,6 +2088,12 @@ spec:
|
||||
- hmac-sha1
|
||||
- hmac-md5
|
||||
type: string
|
||||
clusterRef:
|
||||
description: |-
|
||||
ClusterRef names the BindCluster this key is included in. When empty the
|
||||
key is shared with every cluster in the namespace (useful when multiple
|
||||
clusters share one namespace).
|
||||
type: string
|
||||
importExisting:
|
||||
description: |-
|
||||
ImportExisting, when true, means the referenced Secret already contains a
|
||||
|
||||
@@ -16,6 +16,10 @@ type RenderInput struct {
|
||||
Policies []bindv1alpha1.BindPolicy
|
||||
DNSSECPolicies []bindv1alpha1.BindDNSSECPolicy
|
||||
Catalog *bindv1alpha1.BindCatalogZone
|
||||
// Forwards are type:forward BindZones. They are pure configuration (no
|
||||
// replicated data), so they are rendered into named.conf on every pod
|
||||
// rather than added dynamically to the primary.
|
||||
Forwards []bindv1alpha1.BindZone
|
||||
// PrimaryAddress is the in-cluster address secondaries transfer from.
|
||||
PrimaryAddress string
|
||||
}
|
||||
@@ -90,6 +94,29 @@ func render(in RenderInput, isPrimary bool) string {
|
||||
b.WriteString(renderCatalogZoneDecl(in, isPrimary, ""))
|
||||
}
|
||||
|
||||
// Top-level forward zones (BIND only allows top-level zones when no views
|
||||
// are defined; in-view forward zones are rendered inside renderView).
|
||||
if len(in.Views) == 0 {
|
||||
for _, z := range in.Forwards {
|
||||
if z.Spec.ViewRef == "" {
|
||||
b.WriteString(renderForwardZone(z, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderForwardZone renders a type:forward zone clause.
|
||||
func renderForwardZone(z bindv1alpha1.BindZone, indent string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%szone \"%s\" {\n", indent, z.Spec.ZoneName))
|
||||
b.WriteString(indent + " type forward;\n")
|
||||
b.WriteString(indent + " forward only;\n")
|
||||
if len(z.Spec.Forwarders) > 0 {
|
||||
b.WriteString(fmt.Sprintf("%s forwarders { %s };\n", indent, terminate(z.Spec.Forwarders)))
|
||||
}
|
||||
b.WriteString(indent + "};\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -122,6 +149,12 @@ func renderView(v bindv1alpha1.BindView, in RenderInput, isPrimary bool) string
|
||||
if in.Catalog != nil {
|
||||
b.WriteString(renderCatalogZoneDecl(in, isPrimary, " "))
|
||||
}
|
||||
// Forward zones bound to this view.
|
||||
for _, z := range in.Forwards {
|
||||
if z.Spec.ViewRef == v.Name {
|
||||
b.WriteString(renderForwardZone(z, " "))
|
||||
}
|
||||
}
|
||||
b.WriteString("};\n\n")
|
||||
return b.String()
|
||||
}
|
||||
@@ -206,6 +239,9 @@ func catalogZonesClause(in RenderInput, isPrimary bool, indent string) string {
|
||||
if len(primaries) == 0 && in.PrimaryAddress != "" {
|
||||
primaries = []string{in.PrimaryAddress}
|
||||
}
|
||||
if len(primaries) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(indent + "catalog-zones {\n")
|
||||
b.WriteString(fmt.Sprintf("%s zone \"%s\" default-primaries { %s };\n", indent, in.Catalog.Spec.ZoneName, terminate(primaries)))
|
||||
@@ -227,6 +263,12 @@ func renderCatalogZoneDecl(in RenderInput, isPrimary bool, indent string) string
|
||||
if len(primaries) == 0 && in.PrimaryAddress != "" {
|
||||
primaries = []string{in.PrimaryAddress}
|
||||
}
|
||||
if len(primaries) == 0 {
|
||||
// Primary IP not known yet; omit the secondary catalog zone rather than
|
||||
// emit an invalid empty primaries list. A Pod-triggered reconcile renders
|
||||
// it once the primary pod has an IP.
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%szone \"%s\" {\n", indent, cat.Spec.ZoneName))
|
||||
b.WriteString(indent + " type secondary;\n")
|
||||
|
||||
@@ -53,6 +53,56 @@ func TestRenderCatalogOnSecondaryOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCatalogOmittedWhenPrimaryIPUnknown(t *testing.T) {
|
||||
// Primary IP not known yet and no explicit default-primaries: the secondary
|
||||
// must not emit a catalog-zones / secondary catalog zone with an empty
|
||||
// primaries list (which BIND rejects at config load).
|
||||
in := RenderInput{
|
||||
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
|
||||
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal"}},
|
||||
PrimaryAddress: "",
|
||||
}
|
||||
_, secondary := RenderNamedConf(in)
|
||||
if strings.Contains(secondary, "catalog-zones") || strings.Contains(secondary, "primaries {") {
|
||||
t.Fatalf("secondary must omit catalog primaries when the primary IP is unknown:\n%s", secondary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCatalogUsesPrimaryIP(t *testing.T) {
|
||||
in := RenderInput{
|
||||
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
|
||||
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal"}},
|
||||
PrimaryAddress: "10.42.0.7",
|
||||
}
|
||||
_, secondary := RenderNamedConf(in)
|
||||
if !strings.Contains(secondary, "primaries { 10.42.0.7; }") {
|
||||
t.Fatalf("secondary should point primaries at the primary pod IP:\n%s", secondary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderForwardZoneInView(t *testing.T) {
|
||||
rec := true
|
||||
in := RenderInput{
|
||||
Cluster: newCluster(bindv1alpha1.ModeResolver),
|
||||
Views: []bindv1alpha1.BindView{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "openforwarder"},
|
||||
Spec: bindv1alpha1.BindViewSpec{ClusterRef: "auth", MatchClients: []string{"acl-main"}, Recursion: &rec},
|
||||
}},
|
||||
Forwards: []bindv1alpha1.BindZone{{
|
||||
Spec: bindv1alpha1.BindZoneSpec{ClusterRef: "auth", ZoneName: "unkin.net", Type: bindv1alpha1.ZoneForward, ViewRef: "openforwarder", Forwarders: []string{"198.18.19.15"}},
|
||||
}},
|
||||
}
|
||||
primary, secondary := RenderNamedConf(in)
|
||||
for _, out := range []string{primary, secondary} {
|
||||
if !strings.Contains(out, `view "openforwarder"`) {
|
||||
t.Fatalf("view missing:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, `zone "unkin.net" {`) || !strings.Contains(out, "type forward;") || !strings.Contains(out, "forwarders { 198.18.19.15; }") {
|
||||
t.Fatalf("forward zone not rendered inside view (must be on all pods):\n%s", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderACL(t *testing.T) {
|
||||
in := RenderInput{
|
||||
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
|
||||
|
||||
+10
-8
@@ -26,14 +26,15 @@ func (e *Executor) ZoneExists(ctx context.Context, namespace, pod, zone, view st
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// WriteSeedZone writes a minimal loadable zone file (SOA + apex NS) to path,
|
||||
// creating parent directories. It is only safe to call when creating a zone, as
|
||||
// it overwrites any existing file.
|
||||
func (e *Executor) WriteSeedZone(ctx context.Context, namespace, pod, zone, path, primaryNS string, serial int64) error {
|
||||
// WriteSeedZone writes a minimal loadable zone file (SOA + apex NS + glue) to
|
||||
// path, creating parent directories. The apex NS is the in-zone name ns1, and a
|
||||
// glue A record pointing at primaryIP is included so BIND's check-integrity
|
||||
// accepts the zone (an in-zone NS without an address record is a load error).
|
||||
// It is only safe to call when creating a zone, as it overwrites any existing
|
||||
// file. This is a placeholder that is replaced once real records are loaded.
|
||||
func (e *Executor) WriteSeedZone(ctx context.Context, namespace, pod, zone, path, primaryIP string, serial int64) error {
|
||||
origin := dot(zone)
|
||||
if primaryNS == "" {
|
||||
primaryNS = "ns1." + origin
|
||||
}
|
||||
ns := "ns1." + origin
|
||||
content := fmt.Sprintf(`$TTL 3600
|
||||
@ IN SOA %s hostmaster.%s (
|
||||
%d ; serial
|
||||
@@ -42,7 +43,8 @@ func (e *Executor) WriteSeedZone(ctx context.Context, namespace, pod, zone, path
|
||||
1209600 ; expire
|
||||
300 ) ; minimum
|
||||
@ IN NS %s
|
||||
`, dot(primaryNS), origin, serial, dot(primaryNS))
|
||||
ns1 IN A %s
|
||||
`, ns, origin, serial, ns, primaryIP)
|
||||
|
||||
cmd := []string{"sh", "-c", fmt.Sprintf("mkdir -p \"$(dirname '%s')\" && cat > '%s'", path, path)}
|
||||
if out, err := e.Exec(ctx, namespace, pod, cmd, content); err != nil {
|
||||
|
||||
@@ -50,7 +50,11 @@ func (r *BindCatalogZoneReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
|
||||
// Ensure the catalog zone exists on the primary.
|
||||
if !r.Exec.ZoneExists(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, "") {
|
||||
if err := r.Exec.WriteSeedZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, bind.CatalogFilePath(catalog.Spec.ZoneName), "", 1); err != nil {
|
||||
primaryIP := primaryPodIP(ctx, r.Client, cluster)
|
||||
if primaryIP == "" {
|
||||
return r.fail(ctx, &catalog, "PrimaryNoIP", "waiting for primary pod IP")
|
||||
}
|
||||
if err := r.Exec.WriteSeedZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, bind.CatalogFilePath(catalog.Spec.ZoneName), primaryIP, 1); err != nil {
|
||||
return r.fail(ctx, &catalog, "SeedFailed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,15 @@ func (r *BindClusterReconciler) reconcileKeysSecret(ctx context.Context, c *bind
|
||||
if err := r.List(ctx, &keys, client.InNamespace(c.Namespace)); err != nil {
|
||||
return err
|
||||
}
|
||||
items := append([]bindv1alpha1.BindTSIGKey(nil), keys.Items...)
|
||||
// Include keys scoped to this cluster (spec.clusterRef == name) and shared
|
||||
// keys (empty clusterRef). This keeps keys from leaking across clusters that
|
||||
// share a namespace.
|
||||
var items []bindv1alpha1.BindTSIGKey
|
||||
for _, k := range keys.Items {
|
||||
if k.Spec.ClusterRef == "" || k.Spec.ClusterRef == c.Name {
|
||||
items = append(items, k)
|
||||
}
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
|
||||
|
||||
var b strings.Builder
|
||||
@@ -160,7 +168,10 @@ func (r *BindClusterReconciler) reconcileKeysSecret(ctx context.Context, c *bind
|
||||
}
|
||||
|
||||
func (r *BindClusterReconciler) reconcileConfigMap(ctx context.Context, c *bindv1alpha1.BindCluster) error {
|
||||
in := bind.RenderInput{Cluster: c, PrimaryAddress: primaryAddress(c.Name, c.Namespace)}
|
||||
// BIND primaries/default-primaries need the primary's IP address, not a DNS
|
||||
// name, so render with pod-0's current IP (empty until it is scheduled; the
|
||||
// Pod watch re-renders when it appears or changes).
|
||||
in := bind.RenderInput{Cluster: c, PrimaryAddress: primaryPodIP(ctx, r.Client, c)}
|
||||
|
||||
var acls bindv1alpha1.BindACLList
|
||||
if err := r.List(ctx, &acls, client.InNamespace(c.Namespace)); err == nil {
|
||||
@@ -195,6 +206,17 @@ func (r *BindClusterReconciler) reconcileConfigMap(ctx context.Context, c *bindv
|
||||
}
|
||||
}
|
||||
|
||||
// Forward zones are configuration (no data), so they are rendered into
|
||||
// named.conf on every pod rather than added dynamically to the primary.
|
||||
var zones bindv1alpha1.BindZoneList
|
||||
if err := r.List(ctx, &zones, client.InNamespace(c.Namespace)); err == nil {
|
||||
for _, z := range zones.Items {
|
||||
if z.Spec.ClusterRef == c.Name && z.Spec.Type == bindv1alpha1.ZoneForward {
|
||||
in.Forwards = append(in.Forwards, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var catalogs bindv1alpha1.BindCatalogZoneList
|
||||
if err := r.List(ctx, &catalogs, client.InNamespace(c.Namespace)); err == nil {
|
||||
for i := range catalogs.Items {
|
||||
@@ -251,7 +273,56 @@ func (r *BindClusterReconciler) reconcileServices(ctx context.Context, c *bindv1
|
||||
LoadBalancerIP: c.Spec.Service.LoadBalancerIP,
|
||||
},
|
||||
}
|
||||
return r.upsertService(ctx, c, client)
|
||||
// externalTrafficPolicy is only valid for LoadBalancer/NodePort Services.
|
||||
if svcType == corev1.ServiceTypeLoadBalancer || svcType == corev1.ServiceTypeNodePort {
|
||||
client.Spec.ExternalTrafficPolicy = c.Spec.Service.ExternalTrafficPolicy
|
||||
}
|
||||
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) {
|
||||
@@ -387,6 +458,12 @@ func (r *BindClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Owns(&corev1.Service{}).
|
||||
Owns(&corev1.ConfigMap{}).
|
||||
Owns(&corev1.Secret{}).
|
||||
Watches(&corev1.Pod{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
// Re-render named.conf when a cluster pod's IP appears or changes, so
|
||||
// secondaries always point primaries/default-primaries at the current
|
||||
// primary pod IP.
|
||||
return mapToCluster(o.GetLabels()[clusterLabel], o.GetNamespace())
|
||||
})).
|
||||
Watches(&bindv1alpha1.BindACL{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
return mapToCluster(o.(*bindv1alpha1.BindACL).Spec.ClusterRef, o.GetNamespace())
|
||||
})).
|
||||
@@ -399,6 +476,15 @@ func (r *BindClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Watches(&bindv1alpha1.BindDNSSECPolicy{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
return mapToCluster(o.(*bindv1alpha1.BindDNSSECPolicy).Spec.ClusterRef, o.GetNamespace())
|
||||
})).
|
||||
Watches(&bindv1alpha1.BindZone{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
// Only forward zones affect named.conf; primary/secondary zones are
|
||||
// managed dynamically by the BindZone controller.
|
||||
z := o.(*bindv1alpha1.BindZone)
|
||||
if z.Spec.Type != bindv1alpha1.ZoneForward {
|
||||
return nil
|
||||
}
|
||||
return mapToCluster(z.Spec.ClusterRef, o.GetNamespace())
|
||||
})).
|
||||
Watches(&bindv1alpha1.BindCatalogZone{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
return mapToCluster(o.(*bindv1alpha1.BindCatalogZone).Spec.ClusterRef, o.GetNamespace())
|
||||
})).
|
||||
|
||||
@@ -63,7 +63,11 @@ func (r *BindPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
}
|
||||
|
||||
if !r.Exec.ZoneExists(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, policy.Spec.ViewRef) {
|
||||
if err := r.Exec.WriteSeedZone(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, bind.ZoneFilePath(policy.Spec.ZoneName), "", 1); err != nil {
|
||||
primaryIP := primaryPodIP(ctx, r.Client, cluster)
|
||||
if primaryIP == "" {
|
||||
return r.fail(ctx, &policy, "PrimaryNoIP", "waiting for primary pod IP")
|
||||
}
|
||||
if err := r.Exec.WriteSeedZone(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, bind.ZoneFilePath(policy.Spec.ZoneName), primaryIP, 1); err != nil {
|
||||
return r.fail(ctx, &policy, "SeedFailed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,19 @@ func (r *BindZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Forward zones are pure configuration rendered into named.conf by the
|
||||
// BindCluster controller (on every pod), not added dynamically to the
|
||||
// primary. Nothing to do here beyond reporting readiness.
|
||||
if zone.Spec.Type == bindv1alpha1.ZoneForward {
|
||||
zone.Status.Phase = "Ready"
|
||||
zone.Status.ObservedGeneration = zone.Generation
|
||||
setReady(&zone.Status.Conditions, zone.Generation, true, "Configured", "forward zone rendered into named.conf")
|
||||
if err := r.Status().Update(ctx, &zone); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
cluster, err := getCluster(ctx, r.Client, zone.Namespace, zone.Spec.ClusterRef)
|
||||
if err != nil {
|
||||
return r.setPhase(ctx, &zone, "Error", "ClusterMissing", err.Error())
|
||||
@@ -73,8 +86,12 @@ func (r *BindZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
}
|
||||
|
||||
created := !r.Exec.ZoneExists(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, zone.Spec.ViewRef)
|
||||
if created && zone.Spec.Type == bindv1alpha1.ZonePrimary || (created && zone.Spec.Type == "") {
|
||||
if err := r.Exec.WriteSeedZone(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, bind.ZoneFilePath(zone.Spec.ZoneName), "", 1); err != nil {
|
||||
if created && (zone.Spec.Type == bindv1alpha1.ZonePrimary || zone.Spec.Type == "") {
|
||||
primaryIP := primaryPodIP(ctx, r.Client, cluster)
|
||||
if primaryIP == "" {
|
||||
return r.setPhase(ctx, &zone, "Pending", "PrimaryNoIP", "waiting for primary pod IP")
|
||||
}
|
||||
if err := r.Exec.WriteSeedZone(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, bind.ZoneFilePath(zone.Spec.ZoneName), primaryIP, 1); err != nil {
|
||||
return r.setPhase(ctx, &zone, "Error", "SeedFailed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -87,6 +96,18 @@ func primaryReady(ctx context.Context, c client.Client, cluster *bindv1alpha1.Bi
|
||||
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
|
||||
|
||||
@@ -57,6 +57,7 @@ func (r *BindClusterReconciler) upsertService(ctx context.Context, c *bindv1alph
|
||||
existing.Spec.Selector = desired.Spec.Selector
|
||||
existing.Spec.Type = desired.Spec.Type
|
||||
existing.Spec.LoadBalancerIP = desired.Spec.LoadBalancerIP
|
||||
existing.Spec.ExternalTrafficPolicy = desired.Spec.ExternalTrafficPolicy
|
||||
if desired.Annotations != nil {
|
||||
if existing.Annotations == nil {
|
||||
existing.Annotations = map[string]string{}
|
||||
|
||||
Reference in New Issue
Block a user