diff --git a/internal/bind/render.go b/internal/bind/render.go index b3c7a0f..3d663bb 100644 --- a/internal/bind/render.go +++ b/internal/bind/render.go @@ -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() } diff --git a/internal/bind/render_test.go b/internal/bind/render_test.go index a4ca843..8fb166f 100644 --- a/internal/bind/render_test.go +++ b/internal/bind/render_test.go @@ -80,6 +80,29 @@ func TestRenderCatalogUsesPrimaryIP(t *testing.T) { } } +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), diff --git a/internal/controller/bindcluster_controller.go b/internal/controller/bindcluster_controller.go index 175c4eb..178fab4 100644 --- a/internal/controller/bindcluster_controller.go +++ b/internal/controller/bindcluster_controller.go @@ -206,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 { @@ -416,6 +427,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()) })). diff --git a/internal/controller/bindzone_controller.go b/internal/controller/bindzone_controller.go index 933e2ba..311195b 100644 --- a/internal/controller/bindzone_controller.go +++ b/internal/controller/bindzone_controller.go @@ -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())