package controller import ( "context" "fmt" "strings" "sigs.k8s.io/controller-runtime/pkg/client" bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1" "git.unkin.net/unkin/bind-operator/internal/bind" ) func isPrimaryType(t bindv1alpha1.ZoneType) bool { return t == bindv1alpha1.ZonePrimary || t == "" } // catalogEnabled reports whether a primary zone should be registered in the // cluster catalog zone. func catalogEnabled(zone *bindv1alpha1.BindZone) bool { if !isPrimaryType(zone.Spec.Type) { return false } if zone.Spec.Catalog == nil { return true } return *zone.Spec.Catalog } // fqdn resolves a record owner name relative to a zone origin. func fqdn(name, zone string) string { zone = strings.TrimSuffix(zone, ".") + "." if name == "" || name == "@" { return zone } if strings.HasSuffix(name, ".") { return name } return name + "." + zone } func recordsToUpdates(zone string, records []bindv1alpha1.Record, defaultTTL int32) []bind.RecordUpdate { updates := make([]bind.RecordUpdate, 0, len(records)) for _, rec := range records { ttl := defaultTTL if rec.TTL != nil { ttl = *rec.TTL } updates = append(updates, bind.RecordUpdate{ FQDN: fqdn(rec.Name, zone), Type: rec.Type, TTL: ttl, Values: rec.Values, }) } return updates } // updateKeyName returns the TSIG key name (as used in named.conf) for a zone's // update key, falling back to the object name. func updateKeyName(ctx context.Context, c client.Client, zone *bindv1alpha1.BindZone) string { return tsigKeyName(ctx, c, zone.Namespace, zone.Spec.UpdateKeyRef) } // tsigKeyName resolves a BindTSIGKey object reference to the TSIG key name used // in named.conf (the KeyName override when set, otherwise the object name). // Returns "" for an empty ref, and falls back to the ref if the object cannot be // read. func tsigKeyName(ctx context.Context, c client.Client, namespace, ref string) string { if ref == "" { return "" } var key bindv1alpha1.BindTSIGKey if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: ref}, &key); err != nil { return ref } if key.Spec.KeyName != "" { return key.Spec.KeyName } return ref } // matchListInline renders address-match-list entries on one line. func matchListInline(entries []string) string { return terminateInline(entries) } func terminateInline(entries []string) string { var parts []string for _, e := range entries { e = strings.TrimSpace(strings.TrimRight(e, ";")) if e == "" { continue } parts = append(parts, e+";") } return strings.Join(parts, " ") } // alsoNotifyList renders also-notify entries, each optionally annotated with a // TSIG key so the primary signs its NOTIFYs and secondaries can accept them by // key (`allow-notify { key ... }`) rather than by pod IP. An entry that already // carries a `key` clause is left untouched. func alsoNotifyList(addrs []string, key string) string { key = strings.TrimSpace(key) var parts []string for _, a := range addrs { a = strings.TrimSpace(strings.TrimRight(a, ";")) if a == "" { continue } if key != "" && !strings.Contains(a, " key ") { a = fmt.Sprintf("%s key \"%s\"", a, key) } parts = append(parts, a+";") } return strings.Join(parts, " ") } // absolute qualifies a nameserver name. Unlike record owner names, a // spec.nameservers entry is always a full domain name, never relative to the // zone: an in-zone nameserver is spelled out in full. func absolute(name string) string { return strings.TrimSuffix(name, ".") + "." } // zoneNameservers resolves the names to publish in a zone's apex NS RRset: the // declared nameservers, else the primary's stable in-cluster DNS name. The // fallback is deliberately out-of-zone, so no pod IP is needed as glue. func zoneNameservers(declared []string, cluster *bindv1alpha1.BindCluster) []string { if len(declared) > 0 { return declared } return []string{primaryAddress(cluster.Name, cluster.Namespace) + "."} } // apexNSUpdates returns the dynamic-update ops that keep a zone's apex NS RRset // equal to nameservers, plus removal of the seed's ns1 glue once the zone // declares its own nameservers. Ops colliding with a spec.records entry are // dropped: records are applied afterwards and would re-add them, and the churn // would bump the serial on every reconcile. func apexNSUpdates(zone *bindv1alpha1.BindZone, nameservers []string) []bind.RecordUpdate { owns := func(name, typ string) bool { for _, rec := range zone.Spec.Records { if strings.EqualFold(rec.Type, typ) && fqdn(rec.Name, zone.Spec.ZoneName) == fqdn(name, zone.Spec.ZoneName) { return true } } return false } ttl := zone.Spec.DefaultTTL if ttl <= 0 { ttl = 3600 } var updates []bind.RecordUpdate if !owns("@", "NS") { values := make([]string, 0, len(nameservers)) for _, ns := range nameservers { values = append(values, absolute(ns)) } updates = append(updates, bind.RecordUpdate{FQDN: fqdn("@", zone.Spec.ZoneName), Type: "NS", TTL: ttl, Values: values}) } // The seed's placeholder glue pins a pod IP that goes stale on the first // reschedule; drop it once the zone names its real nameservers. if glue := fqdn("ns1", zone.Spec.ZoneName); len(zone.Spec.Nameservers) > 0 && !owns("ns1", "A") { published := false for _, ns := range nameservers { published = published || absolute(ns) == glue } if !published { updates = append(updates, bind.RecordUpdate{FQDN: glue, Type: "A", Delete: true}) } } return updates }