1
0
Fork 0

refactor: move http api to a new transport layer

This commit is contained in:
Arthur K. 2026-01-23 09:56:01 +03:00
parent 476c4b056f
commit 0448f66ab2
Signed by: wzray
GPG key ID: B97F30FDC4636357
41 changed files with 822 additions and 390 deletions

View file

@ -0,0 +1,96 @@
package host
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"git.wzray.com/homelab/hivemind/internal/config"
"github.com/rs/zerolog/log"
)
type TraefikListener interface {
OnTraefikUpdate(traefikResponse)
}
type TraefikGateway struct {
client *http.Client
server *http.Server
listener TraefikListener
address url.URL
domain string
}
func NewTraefikGateway(cfg config.HostConfig, listener TraefikListener) *TraefikGateway {
mux := http.NewServeMux()
gw := &TraefikGateway{
client: &http.Client{},
server: &http.Server{
Addr: cfg.ListenAddress,
Handler: mux,
},
listener: listener,
address: url.URL{
Scheme: "http",
Host: cfg.LocalAddress,
},
domain: cfg.Domain,
}
mux.HandleFunc("/callback", gw.onCallback)
return gw
}
func (g *TraefikGateway) Listen() error {
return g.server.ListenAndServe()
}
func (g *TraefikGateway) Shutdown(ctx context.Context) error {
return g.server.Shutdown(ctx)
}
func (g *TraefikGateway) GetRawData() (*traefikResponse, error) {
var raw TraefikRawResponse
url := g.address
url.Path = "/api/rawdata"
req := http.Request{
Method: http.MethodGet,
URL: &url,
}
req.Host = g.domain
r, err := g.client.Do(&req)
if err != nil {
return nil, fmt.Errorf("make request: %w", err)
}
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
return nil, fmt.Errorf("unmarshal body: %w", err)
}
out := parseTraefikResponse(raw)
return &out, nil
}
func (g *TraefikGateway) onCallback(w http.ResponseWriter, req *http.Request) {
var raw TraefikRawResponse
if err := json.NewDecoder(req.Body).Decode(&raw); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Err(err).Msg("unable to decode traefik callback data")
return
}
resp := parseTraefikResponse(raw)
if g.listener != nil {
g.listener.OnTraefikUpdate(resp)
}
w.Write([]byte("OK"))
}

View file

@ -2,36 +2,37 @@ package host
import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"sync"
"git.wzray.com/homelab/hivemind/internal/app"
"git.wzray.com/homelab/hivemind/internal/config"
"git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/hivemind/internal/transport"
"git.wzray.com/homelab/hivemind/internal/transport/host"
"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
state *app.State
config config.HostConfig
client *traefikClient
gateway *TraefikGateway
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),
func New(state *app.State, config config.HostConfig) *Role {
r := &Role{
state: state,
config: config,
}
r.gateway = NewTraefikGateway(config, r)
return r
}
func (r *Role) sendUpdate(domains []string, role types.Role) {
@ -45,7 +46,7 @@ func (r *Role) sendUpdate(domains []string, role types.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 {
if _, err := r.state.Clients.DNS.Callback(node.Endpoint, state); err != nil {
logger.Warn().Err(err).Msg("unable to send dns info")
} else {
logger.Debug().Msg("update sent")
@ -54,7 +55,7 @@ func (r *Role) sendUpdate(domains []string, role types.Role) {
}
}
func (r *Role) mutateState(resp traefikResponse) {
func (r *Role) OnTraefikUpdate(resp traefikResponse) {
newInternal := resp.Domains(r.config.InternalEntrypoint)
newExternal := resp.Domains(r.config.ExternalEntrypoint)
@ -71,20 +72,7 @@ func (r *Role) mutateState(resp traefikResponse) {
}
}
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) {
func (r *Role) Dns() (types.HostState, error) {
return types.HostState{
Domains: r.internalDomains,
Address: r.config.IpAddress,
@ -92,7 +80,7 @@ func (r *Role) getInternal() (types.HostState, error) {
}, nil
}
func (r *Role) getExternal() (types.HostState, error) {
func (r *Role) Nameserver() (types.HostState, error) {
return types.HostState{
Domains: r.externalDomains,
Address: r.config.IpAddress,
@ -101,14 +89,25 @@ func (r *Role) getExternal() (types.HostState, error) {
}
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) RegisterHandlers(rg transport.Registrator) {
host.Register(rg, r)
}
func (r *Role) OnStartup(ctx context.Context) error {
resp, err := r.client.GetRawData()
r.tasksGroup.Go(func() {
if err := r.gateway.Listen(); err != nil {
log.Err(err).Msg("traefik gateway stopped")
}
})
r.tasksGroup.Go(func() {
<-ctx.Done()
if err := r.gateway.Shutdown(context.Background()); err != nil {
log.Err(err).Msg("failed to shutdown traefik gateway")
}
})
resp, err := r.gateway.GetRawData()
if err != nil {
return fmt.Errorf("get traefik state: %w", err)
}
@ -116,7 +115,7 @@ func (r *Role) OnStartup(ctx context.Context) error {
log.Info().Msg("got raw data from traefik")
log.Debug().Interface("response", resp).Send()
r.mutateState(*resp)
r.OnTraefikUpdate(*resp)
return nil
}

View file

@ -1,58 +1 @@
package host
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
type traefikClient struct {
client *http.Client
domain string
address url.URL
}
func newClient(domain string, addr string) *traefikClient {
return &traefikClient{
domain: domain,
address: url.URL{
Scheme: "https",
Host: addr,
},
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
ServerName: domain,
},
},
},
}
}
func (c *traefikClient) GetRawData() (*traefikResponse, error) {
var out traefikResponse
url := c.address
url.Path = "/api/rawdata"
req := http.Request{
Method: "GET",
URL: &url,
}
req.Host = c.domain
r, err := c.client.Do(&req)
if err != nil {
return nil, fmt.Errorf("make request: %w", err)
}
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("unmarshal body: %w", err)
}
return &out, nil
}

View file

@ -1,64 +1,66 @@
package host
import (
"encoding/json"
"regexp"
"slices"
)
var hostRegex = regexp.MustCompile("Host\\(`([^()`]+\\.[^()`]+)`\\)")
type rule struct {
type traefikRule struct {
Raw string
Domains []string
Valid bool
}
func (r *rule) UnmarshalJSON(data []byte) error {
r.Valid = false
type traefikRouter struct {
Rule traefikRule
Entrypoints []string
}
raw := ""
if err := json.Unmarshal(data, &raw); err != nil {
return err
type traefikResponse struct {
Routers []traefikRouter
}
type TraefikRawResponse struct {
Routers map[string]TraefikRawRouter `json:"routers"`
}
type TraefikRawRouter struct {
Rule string `json:"rule"`
Entrypoints []string `json:"entryPoints"`
}
func parseTraefikResponse(raw TraefikRawResponse) traefikResponse {
out := traefikResponse{
Routers: make([]traefikRouter, 0, len(raw.Routers)),
}
for _, router := range raw.Routers {
out.Routers = append(out.Routers, traefikRouter{
Rule: parseTraefikRule(router.Rule),
Entrypoints: router.Entrypoints,
})
}
return out
}
func parseTraefikRule(raw string) traefikRule {
rule := traefikRule{
Raw: raw,
}
matches := hostRegex.FindAllStringSubmatch(raw, -1)
for _, match := range matches {
if len(match) <= 1 {
continue
}
r.Domains = append(r.Domains, match[1:]...)
rule.Domains = append(rule.Domains, match[1:]...)
}
r.Valid = len(r.Domains) > 0
return nil
}
type router struct {
Rule rule `json:"rule"`
Entrypoints []string `json:"entryPoints"`
}
type traefikResponse struct {
Routers []router
}
func (r *traefikResponse) UnmarshalJSON(data []byte) error {
var raw struct {
Routers map[string]router `json:"routers"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
for _, v := range raw.Routers {
r.Routers = append(r.Routers, v)
}
return nil
rule.Valid = len(rule.Domains) > 0
return rule
}
func (r traefikResponse) Domains(entrypoint string) []string {