Initial bind-operator: 9 CRDs + controllers
Implements a Kubernetes operator that manages fleets of BIND9 servers declaratively, using controller-runtime (matching forgebot conventions). - add BindCluster reconciler: StatefulSet (pod-0 primary, secondaries), headless + client Services, rendered named.conf ConfigMap, TSIG keys Secret and rndc control Secret; watches dependent CRs to re-render - add BindTSIGKey reconciler that generates key material into a Secret - add BindZone/DNSRecord reconcilers using fully-dynamic delivery (rndc addzone + TSIG nsupdate against the primary pod) - add BindCatalogZone reconciler so secondaries auto-provision zones - add BindPolicy (RPZ), BindDNSSECPolicy, BindView, BindACL reconcilers - render primary/secondary named.conf variants selected by pod ordinal - generate CRDs, deepcopy and RBAC; add samples mapping the three Puppet roles (authoritative/resolver/external-dns) to three BindClusters - add Makefile, Dockerfile.operator, Woodpecker CI and kind manifests
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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 {
|
||||
ref := zone.Spec.UpdateKeyRef
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
var key bindv1alpha1.BindTSIGKey
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: zone.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, " ")
|
||||
}
|
||||
Reference in New Issue
Block a user