From f5ea72740c1b4c7189123a8b486d8e8b5c8747ba Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 15:11:19 +0000 Subject: [PATCH] dns: introduce initial DNS Manager Signed-off-by: Alejandro Mery --- go.mod | 2 +- pkg/dns/dns.go | 36 ++++++++++ pkg/dns/manager.go | 173 +++++++++++++++++++++++++++++++++++++++++++++ pkg/dns/record.go | 80 +++++++++++++++++++++ 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 pkg/dns/manager.go diff --git a/go.mod b/go.mod index 605a3a2..f6f6f05 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/mgechev/revive v1.3.3 github.com/spf13/cobra v1.7.0 golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 gopkg.in/gcfg.v1 v1.2.3 gopkg.in/yaml.v3 v3.0.1 ) @@ -41,7 +42,6 @@ require ( github.com/rs/zerolog v1.30.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.12.0 // indirect diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index ca95946..9fa1ffc 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -1,2 +1,38 @@ // Package dns manages DNS entries for the cluster package dns + +import ( + "fmt" + "net/netip" +) + +// Zone represents a set of hosts with high affinity +type Zone struct { + Name string + + Hosts map[int]*Host +} + +func (z *Zone) String() string { + if z == nil { + return "undetermined" + } + return z.Name +} + +// Host represents a member of the cluster +type Host struct { + zone *Zone + + ID int + Active bool + Addrs []netip.Addr +} + +func (p *Host) String() string { + if p == nil { + return "undetermined" + } + + return fmt.Sprintf("%s-%v", p.zone, p.ID) +} diff --git a/pkg/dns/manager.go b/pkg/dns/manager.go new file mode 100644 index 0000000..8e29252 --- /dev/null +++ b/pkg/dns/manager.go @@ -0,0 +1,173 @@ +package dns + +import ( + "context" + "errors" + "io/fs" + "net/netip" + "strings" + + "darvaza.org/core" + "darvaza.org/slog" + "git.jpi.io/amery/jpictl/pkg/cluster" + "golang.org/x/net/publicsuffix" +) + +// Manager is a DNS Manager instance +type Manager struct { + domain string + suffix string + zones map[string]*Zone + regions map[string][]string + + p Provider + l slog.Logger +} + +// ManagerOption configures a Manager +type ManagerOption func(*Manager) error + +func newErrorManagerOption(err error, hint string) ManagerOption { + return func(*Manager) error { + return core.Wrap(err, hint) + } +} + +// WithProvider attaches a libdns Provider to the Manager +func WithProvider(p Provider) ManagerOption { + var err error + + if p == nil { + p, err = DefaultDNSProvider() + } + + if err != nil { + return newErrorManagerOption(err, "WithProvider") + } + + return func(mgr *Manager) error { + mgr.p = p + return nil + } +} + +// WithLogger attaches a logger to the Manager +func WithLogger(log slog.Logger) ManagerOption { + if log == nil { + log = cluster.DefaultLogger() + } + + return func(mgr *Manager) error { + mgr.l = log + return nil + } +} + +func (mgr *Manager) setDefaults() error { + var opts []ManagerOption + + if mgr.l == nil { + opts = append(opts, WithLogger(nil)) + } + + if mgr.domain == "" || mgr.suffix == "" { + return errors.New("domain not specified") + } + + for _, opt := range opts { + if err := opt(mgr); err != nil { + return err + } + } + return nil +} + +// WithDomain specifies where the manager operates +func WithDomain(domain string) ManagerOption { + base, err := publicsuffix.EffectiveTLDPlusOne(domain) + if err != nil { + return newErrorManagerOption(err, "publicsuffix") + } + + suffix := strings.TrimSuffix(domain, base) + if suffix != "" { + suffix = "." + suffix[:len(suffix)-1] + } + + return func(mgr *Manager) error { + mgr.domain = base + mgr.suffix = suffix + return nil + } +} + +// NewManager creates a DNS manager with the +func NewManager(opts ...ManagerOption) (*Manager, error) { + mgr := &Manager{ + zones: make(map[string]*Zone), + regions: make(map[string][]string), + } + + for _, opt := range opts { + if err := opt(mgr); err != nil { + return nil, err + } + } + + if err := mgr.setDefaults(); err != nil { + return nil, err + } + return mgr, nil +} + +// AddHost registers a host +func (mgr *Manager) AddHost(_ context.Context, zone string, id int, + active bool, addrs ...netip.Addr) error { + // + if zone == "" || id <= 0 { + return fs.ErrInvalid + } + + z, ok := mgr.zones[zone] + if !ok { + z = &Zone{ + Name: zone, + Hosts: make(map[int]*Host), + } + + mgr.zones[zone] = z + } + + p := &Host{ + zone: z, + ID: id, + Active: active, + Addrs: SortAddrSlice(addrs), + } + z.Hosts[id] = p + + if log, ok := mgr.l.Debug().WithEnabled(); ok { + log.WithField("subsystem", "dns"). + WithField("zone", zone). + WithField("host", p.String()). + WithField("active", active). + Print() + } + + return nil +} + +// AddRegion specifies a new region and the zones it contains +func (mgr *Manager) AddRegion(_ context.Context, region string, zones ...string) error { + mgr.regions[region] = append(mgr.regions[region], zones...) + + if log, ok := mgr.l.Debug().WithEnabled(); ok { + for _, zoneName := range zones { + log.WithField("subsystem", "dns"). + WithField("region", region). + WithField("zone", zoneName).Print() + } + } + + return nil +} diff --git a/pkg/dns/record.go b/pkg/dns/record.go index 4b8deeb..407844f 100644 --- a/pkg/dns/record.go +++ b/pkg/dns/record.go @@ -113,3 +113,83 @@ func (rr *AddrRecord) String() string { _, _ = rr.WriteTo(&buf) return buf.String() } + +func (mgr *Manager) genRegionsSorted() []string { + regions := make([]string, 0, len(mgr.regions)) + for name := range mgr.regions { + regions = append(regions, name) + } + + return SortRegions(regions) +} + +func (mgr *Manager) genAllAddrRecords() []AddrRecord { + var out []AddrRecord + + cache := make(map[string][]netip.Addr) + + // zones + for _, z := range mgr.zones { + // hosts + s := mgr.genZoneHostRecords(z) + out = append(out, s...) + + // zone alias + addrs := mgr.genZoneAddresses(z) + name := z.Name + + out = append(out, AddrRecord{ + Name: name + mgr.suffix, + Addr: addrs, + }) + + // and cache for regions + cache[name] = addrs + } + + for _, name := range mgr.genRegionsSorted() { + var addrs []netip.Addr + + for _, z := range mgr.regions[name] { + addrs = append(addrs, cache[z]...) + } + + rec := AddrRecord{ + Name: name + mgr.suffix, + Addr: addrs, + } + rec.Sort() + + out = append(out, rec) + } + + return out +} + +func (*Manager) genZoneAddresses(z *Zone) []netip.Addr { + var out []netip.Addr + + for _, p := range z.Hosts { + if p.Active { + out = append(out, p.Addrs...) + } + } + + SortAddrSlice(out) + return out +} + +func (mgr *Manager) genZoneHostRecords(z *Zone) []AddrRecord { + out := make([]AddrRecord, 0, len(z.Hosts)) + + for _, p := range z.Hosts { + rec := AddrRecord{ + Name: p.String() + mgr.suffix, + Addr: p.Addrs, + } + out = append(out, rec) + } + + SortAddrRecords(out) + return out +}