Files
bind-operator/internal/bind/nsupdate.go
T
unkinben fe5fbdaf6d 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
2026-07-03 15:48:13 +10:00

61 lines
1.9 KiB
Go

package bind
import (
"context"
"fmt"
"strings"
)
// TSIGCreds carries the material needed to authenticate a dynamic update.
type TSIGCreds struct {
Name string // TSIG key name
Algorithm string // e.g. hmac-sha256
Secret string // base64-encoded key
}
// RecordUpdate describes a desired record set to apply to a zone.
type RecordUpdate struct {
FQDN string // fully-qualified owner name, trailing dot recommended
Type string // RR type
TTL int32 // record TTL
Values []string // RDATA entries
Delete bool // when true, delete the RRset instead of replacing it
}
// NSUpdate applies a set of record changes to zone by executing nsupdate on the
// primary pod, targeting the local server and authenticating with creds. All
// changes are sent in a single atomic transaction.
func (e *Executor) NSUpdate(ctx context.Context, namespace, pod, zone string, creds TSIGCreds, updates []RecordUpdate) error {
var b strings.Builder
b.WriteString("server 127.0.0.1\n")
b.WriteString(fmt.Sprintf("zone %s\n", dot(zone)))
for _, u := range updates {
// Replace semantics: clear the RRset first, then add the desired values.
b.WriteString(fmt.Sprintf("update delete %s %s\n", dot(u.FQDN), u.Type))
if u.Delete {
continue
}
for _, v := range u.Values {
b.WriteString(fmt.Sprintf("update add %s %d %s %s\n", dot(u.FQDN), u.TTL, u.Type, v))
}
}
b.WriteString("send\n")
cmd := []string{"nsupdate", "-y", fmt.Sprintf("%s:%s:%s", creds.Algorithm, creds.Name, creds.Secret)}
if out, err := e.Exec(ctx, namespace, pod, cmd, b.String()); err != nil {
return fmt.Errorf("nsupdate zone %s: %w (out: %s)", zone, err, out)
}
return nil
}
// dot ensures a name is fully qualified with a trailing dot.
func dot(name string) string {
if name == "" || name == "@" {
return "@"
}
if strings.HasSuffix(name, ".") {
return name
}
return name + "."
}