package host import ( "context" "encoding/json" "fmt" "net/http" "slices" "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" ) type Role struct { state *state.RuntimeState config config.HostConfig client *traefikClient tasksGroup sync.WaitGroup externalDomains []string // TODO: i don't like hardcoding external/internal logic here internalDomains []string } func New(state *state.RuntimeState, config config.HostConfig) *Role { return &Role{ client: newClient(config.Domain, config.LocalAddress), state: state, config: config, } } func (r *Role) sendUpdate(domains []string, role types.Role) { state := types.HostState{ Domains: domains, Address: r.config.IpAddress, Hostname: r.state.Self.Hostname, } for _, node := range r.state.Registry.ByRole(role) { r.tasksGroup.Go(func() { logger := log.With().Str("name", node.Hostname).Logger() logger.Debug().Msg("sending update") if _, err := client.Post[any](node.Endpoint, types.PathDnsCallback, state); err != nil { logger.Warn().Err(err).Msg("unable to send dns info") } else { logger.Debug().Msg("update sent") } }) } } func (r *Role) mutateState(resp traefikResponse) { newInternal := resp.Domains(r.config.InternalEntrypoint) newExternal := resp.Domains(r.config.ExternalEntrypoint) if !slices.Equal(newInternal, r.internalDomains) { log.Info().Msg("internal domains updated, propogating") r.internalDomains = newInternal r.sendUpdate(newInternal, types.DnsRole) } if !slices.Equal(newExternal, r.externalDomains) { log.Info().Msg("internal domains updated, propogating") r.externalDomains = newExternal r.sendUpdate(newExternal, types.NameserverRole) } } func (r *Role) onCallback(w http.ResponseWriter, req *http.Request) { var resp traefikResponse if err := json.NewDecoder(req.Body).Decode(&resp); err != nil { w.WriteHeader(http.StatusInternalServerError) log.Err(err).Msg("unable to decode traefik callback data") return } r.mutateState(resp) w.Write([]byte("OK")) } func (r *Role) getInternal() (types.HostState, error) { return types.HostState{ Domains: r.internalDomains, Address: r.config.IpAddress, Hostname: r.state.Self.Hostname, }, nil } func (r *Role) getExternal() (types.HostState, error) { return types.HostState{}, nil } func (r *Role) RegisterHandlers(rg types.Registrator) { rg.RegisterRaw(http.MethodPost, types.PathHostCallback.String(), r.onCallback) rg.Register(types.GetEndpoint(types.PathHostDns, r.getInternal)) rg.Register(types.GetEndpoint(types.PathHostNs, r.getExternal)) } func (r *Role) OnStartup(ctx context.Context) error { resp, err := r.client.GetRawData() if err != nil { return fmt.Errorf("get traefik state: %w", err) } log.Info().Msg("got raw data from traefik") log.Debug().Interface("response", resp).Send() r.mutateState(*resp) return nil } func (r *Role) OnShutdown() error { r.tasksGroup.Wait() return nil }