Fix zone provisioning: seed glue + IP primaries #3
@@ -206,6 +206,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 +230,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,33 @@ 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 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,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 {
|
||||
@@ -387,6 +390,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())
|
||||
})).
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,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
|
||||
|
||||
Reference in New Issue
Block a user