feat: initial release
This commit is contained in:
parent
a3cf21f5bd
commit
761174d035
41 changed files with 2008 additions and 217 deletions
121
internal/roles/host/host.go
Normal file
121
internal/roles/host/host.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"git.wzray.com/homelab/mastermind/internal/config"
|
||||
"git.wzray.com/homelab/mastermind/internal/state"
|
||||
"git.wzray.com/homelab/mastermind/internal/types"
|
||||
"git.wzray.com/homelab/mastermind/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.IpAddress),
|
||||
state: state,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Role) sendUpdate(domains []string, role types.Role) {
|
||||
state := types.HostState{
|
||||
Domains: domains,
|
||||
Name: r.state.Self.Name,
|
||||
Endpoint: r.state.Self.Address,
|
||||
}
|
||||
|
||||
for _, node := range r.state.Registry.ByRole(role) {
|
||||
r.tasksGroup.Go(func() {
|
||||
logger := log.With().Str("name", node.Name).Logger()
|
||||
logger.Debug().Msg("sending update")
|
||||
if _, err := client.Post[any](node.Address, 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,
|
||||
Endpoint: r.state.Self.Address,
|
||||
Name: r.state.Self.Name,
|
||||
}, 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
|
||||
}
|
||||
58
internal/roles/host/http.go
Normal file
58
internal/roles/host/http.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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
|
||||
}
|
||||
74
internal/roles/host/types.go
Normal file
74
internal/roles/host/types.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package host
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"slices"
|
||||
)
|
||||
|
||||
var hostRegex = regexp.MustCompile("Host\\(`([^()`]+\\.[^()`]+)`\\)")
|
||||
|
||||
type rule struct {
|
||||
Raw string
|
||||
Domains []string
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func (r *rule) UnmarshalJSON(data []byte) error {
|
||||
r.Valid = false
|
||||
|
||||
raw := ""
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := hostRegex.FindAllStringSubmatch(raw, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) <= 1 {
|
||||
continue
|
||||
}
|
||||
r.Domains = append(r.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
|
||||
}
|
||||
|
||||
func (r traefikResponse) Domains(entrypoint string) []string {
|
||||
out := make([]string, 0, len(r.Routers))
|
||||
|
||||
for _, router := range r.Routers {
|
||||
if router.Rule.Valid && slices.Contains(router.Entrypoints, entrypoint) {
|
||||
out = append(out, router.Rule.Domains...)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue