package dns import ( "context" "io/fs" "net/netip" "strings" "darvaza.org/core" "darvaza.org/slog" "github.com/libdns/libdns" "golang.org/x/net/publicsuffix" "git.jpi.io/amery/jpictl/pkg/cluster" ) // 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 ErrNoDomain } 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 } // GetRecords pulls all the address records on DNS for our domain, // optionally only those matching the given names. func (mgr *Manager) GetRecords(ctx context.Context, names ...string) ([]libdns.Record, error) { if mgr.p == nil { return nil, ErrNoDNSProvider } recs, err := mgr.p.GetRecords(ctx, mgr.domain) switch { case err != nil: // failed return nil, err case len(recs) == 0: // empty return []libdns.Record{}, nil case mgr.suffix == "" && len(names) == 0: // unfiltered return recs, nil default: // filtered recs = mgr.filterRecords(recs, names...) return recs, nil } } func (mgr *Manager) filterRecords(recs []libdns.Record, names ...string) []libdns.Record { out := make([]libdns.Record, 0, len(recs)) for _, rr := range recs { name, ok := mgr.matchSuffix(rr) switch { case !ok: // skip, wrong subdomain continue case len(names) == 0: // unfiltered, take it case !core.SliceContains(names, name): // skip, not one of the requested names continue } out = append(out, rr) } return out } func (mgr *Manager) matchSuffix(rr libdns.Record) (string, bool) { if mgr.suffix == "" { // no suffix return rr.Name, true } // remove suffix return strings.CutSuffix(rr.Name, mgr.suffix) } // 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 }