package dns import ( "context" "fmt" "os" "os/exec" "strings" "sync" "git.wzray.com/homelab/hivemind/internal/config" "git.wzray.com/homelab/hivemind/internal/state" "git.wzray.com/homelab/hivemind/internal/types" "git.wzray.com/homelab/hivemind/internal/web/client" "github.com/rs/zerolog/log" ) const hostsDir = "/etc/hosts.d/" type Role struct { state *state.RuntimeState config config.DnsConfig group sync.WaitGroup } func New(state *state.RuntimeState, config config.DnsConfig) *Role { r := &Role{ state: state, config: config, } return r } func (r *Role) updateDnsmasq(filename string, data []byte) error { if err := os.WriteFile(filename, data, 0644); err != nil { return fmt.Errorf("write endpoint file %q: %w", filename, err) } if err := r.reload(); err != nil { return fmt.Errorf("reload dnsmasq: %w", err) } return nil } func parseState(state types.HostState) (string, []byte) { var builder strings.Builder for _, d := range state.Domains { builder.WriteString(fmt.Sprintf("%s %s\n", state.Address, d)) } return hostsDir + state.Hostname, []byte(builder.String()) } func (r *Role) OnStartup(ctx context.Context) error { r.group.Go(func() { r.syncFromRegistry() }) c := r.state.Registry.OnChanged() r.group.Go(func() { for { select { case <-ctx.Done(): return case <-c: r.syncFromRegistry() } } }) return nil } func (r *Role) syncFromRegistry() { for _, n := range r.state.Registry.ByRole(types.HostRole) { state, err := client.Get[types.HostState](n.Endpoint, types.PathHostDns) if err != nil { log.Warn().Str("name", n.Hostname).Err(err).Msg("unable to get host config") continue } filename, data := parseState(*state) if err := r.updateDnsmasq(filename, data); err != nil { log.Warn().Str("name", n.Hostname).Err(err).Msg("unable to update dnsmasq") continue } } } func (r *Role) OnShutdown() error { r.group.Wait() return nil } func (r *Role) reload() error { var err error if r.config.UseSystemd { err = exec.Command("systemctl", "reload", "dnsmasq").Run() } else { err = exec.Command("/etc/init.d/dnsmasq", "reload").Run() } return err } func (r *Role) onCallback(state types.HostState) (bool, error) { filename, data := parseState(state) if err := r.updateDnsmasq(filename, data); err != nil { return false, err } return true, nil } func (r *Role) RegisterHandlers(rg types.Registrator) { rg.Register(types.PostEndpoint(types.PathDnsCallback, r.onCallback)) }