diff --git a/internal/handlers/domains.go b/internal/handlers/domains.go new file mode 100644 index 0000000..9c56a88 --- /dev/null +++ b/internal/handlers/domains.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "net/http" + "time" +) + +// domainResponse represents a single domain in the API response. +type domainResponse struct { + Domain string `json:"domain"` + Nameservers []string `json:"nameservers,omitempty"` + LastChecked string `json:"lastChecked,omitempty"` + Status string `json:"status"` +} + +// domainsResponse is the top-level response for GET /api/v1/domains. +type domainsResponse struct { + Domains []domainResponse `json:"domains"` +} + +// HandleDomains returns the configured domains and their status. +func (h *Handlers) HandleDomains() http.HandlerFunc { + return func( + writer http.ResponseWriter, + request *http.Request, + ) { + configured := h.config.Domains + snapshot := h.state.GetSnapshot() + + domains := make( + []domainResponse, 0, len(configured), + ) + + for _, domain := range configured { + dr := domainResponse{ + Domain: domain, + Status: "pending", + } + + ds, ok := snapshot.Domains[domain] + if ok { + dr.Nameservers = ds.Nameservers + dr.Status = "ok" + + if !ds.LastChecked.IsZero() { + dr.LastChecked = ds.LastChecked. + Format(time.RFC3339) + } + } + + domains = append(domains, dr) + } + + h.respondJSON( + writer, request, + &domainsResponse{Domains: domains}, + http.StatusOK, + ) + } +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 474c1bc..a791d9e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -8,9 +8,11 @@ import ( "go.uber.org/fx" + "sneak.berlin/go/dnswatcher/internal/config" "sneak.berlin/go/dnswatcher/internal/globals" "sneak.berlin/go/dnswatcher/internal/healthcheck" "sneak.berlin/go/dnswatcher/internal/logger" + "sneak.berlin/go/dnswatcher/internal/state" ) // Params contains dependencies for Handlers. @@ -20,6 +22,8 @@ type Params struct { Logger *logger.Logger Globals *globals.Globals Healthcheck *healthcheck.Healthcheck + State *state.State + Config *config.Config } // Handlers provides HTTP request handlers. @@ -28,6 +32,8 @@ type Handlers struct { params *Params globals *globals.Globals hc *healthcheck.Healthcheck + state *state.State + config *config.Config } // New creates a new Handlers instance. @@ -37,6 +43,8 @@ func New(_ fx.Lifecycle, params Params) (*Handlers, error) { params: ¶ms, globals: params.Globals, hc: params.Healthcheck, + state: params.State, + config: params.Config, }, nil } @@ -44,7 +52,7 @@ func (h *Handlers) respondJSON( writer http.ResponseWriter, _ *http.Request, data any, - status int, + status int, //nolint:unparam // general-purpose utility; status varies in future use ) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(status) diff --git a/internal/handlers/hostnames.go b/internal/handlers/hostnames.go new file mode 100644 index 0000000..58a8402 --- /dev/null +++ b/internal/handlers/hostnames.go @@ -0,0 +1,120 @@ +package handlers + +import ( + "net/http" + "sort" + "time" + + "sneak.berlin/go/dnswatcher/internal/state" +) + +// nameserverRecordResponse represents one nameserver's records +// in the API response. +type nameserverRecordResponse struct { + Nameserver string `json:"nameserver"` + Records map[string][]string `json:"records"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + LastChecked string `json:"lastChecked,omitempty"` +} + +// hostnameResponse represents a single hostname in the API response. +type hostnameResponse struct { + Hostname string `json:"hostname"` + Nameservers []nameserverRecordResponse `json:"nameservers,omitempty"` + LastChecked string `json:"lastChecked,omitempty"` + Status string `json:"status"` +} + +// hostnamesResponse is the top-level response for +// GET /api/v1/hostnames. +type hostnamesResponse struct { + Hostnames []hostnameResponse `json:"hostnames"` +} + +// HandleHostnames returns the configured hostnames and their status. +func (h *Handlers) HandleHostnames() http.HandlerFunc { + return func( + writer http.ResponseWriter, + request *http.Request, + ) { + configured := h.config.Hostnames + snapshot := h.state.GetSnapshot() + + hostnames := make( + []hostnameResponse, 0, len(configured), + ) + + for _, hostname := range configured { + hr := hostnameResponse{ + Hostname: hostname, + Status: "pending", + } + + hs, ok := snapshot.Hostnames[hostname] + if ok { + hr.Status = "ok" + + if !hs.LastChecked.IsZero() { + hr.LastChecked = hs.LastChecked. + Format(time.RFC3339) + } + + hr.Nameservers = buildNameserverRecords( + hs, + ) + } + + hostnames = append(hostnames, hr) + } + + h.respondJSON( + writer, request, + &hostnamesResponse{Hostnames: hostnames}, + http.StatusOK, + ) + } +} + +// buildNameserverRecords converts the per-nameserver state map +// into a sorted slice for deterministic JSON output. +func buildNameserverRecords( + hs *state.HostnameState, +) []nameserverRecordResponse { + if hs.RecordsByNameserver == nil { + return nil + } + + nsNames := make( + []string, 0, len(hs.RecordsByNameserver), + ) + for ns := range hs.RecordsByNameserver { + nsNames = append(nsNames, ns) + } + + sort.Strings(nsNames) + + records := make( + []nameserverRecordResponse, 0, len(nsNames), + ) + + for _, ns := range nsNames { + nsr := hs.RecordsByNameserver[ns] + + entry := nameserverRecordResponse{ + Nameserver: ns, + Records: nsr.Records, + Status: nsr.Status, + Error: nsr.Error, + } + + if !nsr.LastChecked.IsZero() { + entry.LastChecked = nsr.LastChecked. + Format(time.RFC3339) + } + + records = append(records, entry) + } + + return records +} diff --git a/internal/server/routes.go b/internal/server/routes.go index cd07ba1..df5d5c9 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -28,6 +28,8 @@ func (s *Server) SetupRoutes() { // API v1 routes s.router.Route("/api/v1", func(r chi.Router) { r.Get("/status", s.handlers.HandleStatus()) + r.Get("/domains", s.handlers.HandleDomains()) + r.Get("/hostnames", s.handlers.HandleHostnames()) }) // Metrics endpoint (optional, with basic auth)