1
0
Fork 0

Compare commits

..

2 commits

22 changed files with 127 additions and 108 deletions

View file

@ -9,18 +9,18 @@ import (
"strconv" "strconv"
"syscall" "syscall"
"git.wzray.com/homelab/mastermind/internal/config" "git.wzray.com/homelab/hivemind/internal/config"
"git.wzray.com/homelab/mastermind/internal/registry" "git.wzray.com/homelab/hivemind/internal/registry"
"git.wzray.com/homelab/mastermind/internal/roles" "git.wzray.com/homelab/hivemind/internal/roles"
"git.wzray.com/homelab/mastermind/internal/roles/dns" "git.wzray.com/homelab/hivemind/internal/roles/dns"
"git.wzray.com/homelab/mastermind/internal/roles/host" "git.wzray.com/homelab/hivemind/internal/roles/host"
"git.wzray.com/homelab/mastermind/internal/roles/master" "git.wzray.com/homelab/hivemind/internal/roles/master"
"git.wzray.com/homelab/mastermind/internal/roles/node" "git.wzray.com/homelab/hivemind/internal/roles/node"
"git.wzray.com/homelab/mastermind/internal/state" "git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/client" "git.wzray.com/homelab/hivemind/internal/web/client"
"git.wzray.com/homelab/mastermind/internal/web/middleware" "git.wzray.com/homelab/hivemind/internal/web/middleware"
"git.wzray.com/homelab/mastermind/internal/web/server" "git.wzray.com/homelab/hivemind/internal/web/server"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors" "github.com/rs/zerolog/pkgerrors"
@ -76,8 +76,9 @@ func main() {
zerolog.SetGlobalLevel(levelToZerolog(config.Node.LogLevel)) zerolog.SetGlobalLevel(levelToZerolog(config.Node.LogLevel))
self := types.NewNode( self := types.NewNode(
fmt.Sprintf("%v:%v", config.Node.Endpoint, config.Node.Port),
config.Node.Hostname, config.Node.Hostname,
config.Node.Address,
config.Node.Port,
config.Roles, config.Roles,
) )

View file

@ -1,19 +1,19 @@
[node] [node]
log_level = "DEBUG" log_level = "DEBUG"
hostname = "" address = "laptop.spb.wzray.com"
endpoint = "" hostname = "laptop.spb.wzray.com"
bootstrap_master = "" bootstrap_master = "hicpu.spb.wzray.com"
keepalive_interval = 1 keepalive_interval = 10
[roles.master] [roles.master]
observer_interval = 4 observer_interval = 1
[roles.dns] [roles.dns]
use_systemd = false use_systemd = false
[roles.host] [roles.host]
domain = "" domain = "traefik.wzray.com"
ip = "" ip = "10.161.4.11"
local_address = "" local_address = "10.161.4.11"
internal_entrypoint = "" internal_entrypoint = "https"
external_entrypoint = "" external_entrypoint = "ehttps"

2
go.mod
View file

@ -1,4 +1,4 @@
module git.wzray.com/homelab/mastermind module git.wzray.com/homelab/hivemind
go 1.25.5 go 1.25.5

View file

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )

View file

@ -3,6 +3,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
"strings" "strings"
) )
@ -28,29 +29,34 @@ func (l *LogLevel) UnmarshalText(data []byte) error {
} }
type NodeConfig struct { type NodeConfig struct {
Hostname string `toml:"hostname"` Hostname string `toml:"hostname"`
Endpoint string `toml:"endpoint"` Address string `toml:"address"`
Port int `toml:"port"`
KeepaliveInterval int `toml:"keepalive_interval"` KeepaliveInterval int `toml:"keepalive_interval"`
LogLevel LogLevel `toml:"log_level"` LogLevel LogLevel `toml:"log_level"`
BootstrapMaster string `toml:"bootstrap_master"` BootstrapMaster string `toml:"bootstrap_master"`
ListenOn string `toml:"listen_on"` ListenOn string `toml:"listen_on"`
Port int `toml:"port"`
} }
func (c NodeConfig) Validate() error { func (c NodeConfig) Validate() error {
if c.Address == "" {
return errors.New("missing address")
}
if c.Hostname == "" { if c.Hostname == "" {
return errors.New("missing hostname") return errors.New("missing hostname")
} }
if c.Endpoint == "" {
return errors.New("missing endpoint")
}
if c.KeepaliveInterval < 1 && c.KeepaliveInterval != -1 { if c.KeepaliveInterval < 1 && c.KeepaliveInterval != -1 {
return errors.New("invalid keepalive_interval") return errors.New("invalid keepalive_interval")
} }
if net.ParseIP(c.ListenOn) == nil {
return errors.New("invalid listen_on")
}
return nil return nil
} }
@ -59,8 +65,8 @@ func (c *NodeConfig) Merge(other NodeConfig) {
c.Hostname = other.Hostname c.Hostname = other.Hostname
} }
if other.Endpoint != "" { if other.Address != "" {
c.Endpoint = other.Endpoint c.Address = other.Address
} }
if other.BootstrapMaster != "" { if other.BootstrapMaster != "" {

View file

@ -8,7 +8,7 @@ import (
"sync" "sync"
"time" "time"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
) )
type FileStorage struct { type FileStorage struct {

View file

@ -6,7 +6,7 @@ import (
"sync" "sync"
"time" "time"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -36,7 +36,7 @@ func New(storage Storage, self types.Node) *Registry {
r.nodes = storedData.Nodes r.nodes = storedData.Nodes
ret: ret:
r.nodes[self.Name] = self r.nodes[self.Hostname] = self
return r return r
} }
@ -68,7 +68,7 @@ func (r *Registry) AllNodes() []types.Node {
func (r *Registry) Nodes() []types.Node { func (r *Registry) Nodes() []types.Node {
nodes := r.AllNodes() nodes := r.AllNodes()
nodes = slices.DeleteFunc(nodes, func(n types.Node) bool { nodes = slices.DeleteFunc(nodes, func(n types.Node) bool {
return n.Name == r.self.Name return n.Hostname == r.self.Hostname
}) })
return nodes return nodes
} }
@ -79,7 +79,7 @@ func (r *Registry) ByRole(role types.Role) []types.Node {
o := make([]types.Node, 0, len(r.nodes)) o := make([]types.Node, 0, len(r.nodes))
for _, node := range r.nodes { for _, node := range r.nodes {
if slices.Contains(node.Roles, role) && node.Name != r.self.Name { if slices.Contains(node.Roles, role) && node.Hostname != r.self.Hostname {
o = append(o, node) o = append(o, node)
} }
} }
@ -88,7 +88,7 @@ func (r *Registry) ByRole(role types.Role) []types.Node {
func (r *Registry) AddNode(node types.Node) error { func (r *Registry) AddNode(node types.Node) error {
r.lock.Lock() r.lock.Lock()
r.nodes[node.Name] = node r.nodes[node.Hostname] = node
r.LastUpdate = time.Now() r.LastUpdate = time.Now()
snapshot := r.snapshot() snapshot := r.snapshot()
r.lock.Unlock() r.lock.Unlock()
@ -119,7 +119,7 @@ func (r *Registry) Set(nodes []types.Node) error {
r.lock.Lock() r.lock.Lock()
r.nodes = make(map[string]types.Node) r.nodes = make(map[string]types.Node)
for _, n := range nodes { for _, n := range nodes {
r.nodes[n.Name] = n r.nodes[n.Hostname] = n
} }
snapshot := r.snapshot() snapshot := r.snapshot()
r.lock.Unlock() r.lock.Unlock()

View file

@ -1,6 +1,6 @@
package registry package registry
import "git.wzray.com/homelab/mastermind/internal/types" import "git.wzray.com/homelab/hivemind/internal/types"
type Storage interface { type Storage interface {
Save(*storedConfig) error Save(*storedConfig) error

View file

@ -8,10 +8,10 @@ import (
"strings" "strings"
"sync" "sync"
"git.wzray.com/homelab/mastermind/internal/config" "git.wzray.com/homelab/hivemind/internal/config"
"git.wzray.com/homelab/mastermind/internal/state" "git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/client" "git.wzray.com/homelab/hivemind/internal/web/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -48,10 +48,10 @@ func parseState(state types.HostState) (string, []byte) {
var builder strings.Builder var builder strings.Builder
for _, d := range state.Domains { for _, d := range state.Domains {
builder.WriteString(fmt.Sprintf("%s %s\n", state.Endpoint, d)) builder.WriteString(fmt.Sprintf("%s %s\n", state.Address, d))
} }
return hostsDir + state.Name, []byte(builder.String()) return hostsDir + state.Hostname, []byte(builder.String())
} }
func (r *Role) OnStartup(ctx context.Context) error { func (r *Role) OnStartup(ctx context.Context) error {
@ -76,15 +76,15 @@ func (r *Role) OnStartup(ctx context.Context) error {
func (r *Role) syncFromRegistry() { func (r *Role) syncFromRegistry() {
for _, n := range r.state.Registry.ByRole(types.HostRole) { for _, n := range r.state.Registry.ByRole(types.HostRole) {
state, err := client.Get[types.HostState](n.Address, types.PathHostDns) state, err := client.Get[types.HostState](n.Endpoint, types.PathHostDns)
if err != nil { if err != nil {
log.Warn().Str("name", n.Name).Err(err).Msg("unable to get host config") log.Warn().Str("name", n.Hostname).Err(err).Msg("unable to get host config")
continue continue
} }
filename, data := parseState(*state) filename, data := parseState(*state)
if err := r.updateDnsmasq(filename, data); err != nil { if err := r.updateDnsmasq(filename, data); err != nil {
log.Warn().Str("name", n.Name).Err(err).Msg("unable to update dnsmasq") log.Warn().Str("name", n.Hostname).Err(err).Msg("unable to update dnsmasq")
continue continue
} }
} }

View file

@ -8,19 +8,19 @@ import (
"slices" "slices"
"sync" "sync"
"git.wzray.com/homelab/mastermind/internal/config" "git.wzray.com/homelab/hivemind/internal/config"
"git.wzray.com/homelab/mastermind/internal/state" "git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/client" "git.wzray.com/homelab/hivemind/internal/web/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type Role struct { type Role struct {
state *state.RuntimeState state *state.RuntimeState
config config.HostConfig config config.HostConfig
client *traefikClient client *traefikClient
tasksGroup sync.WaitGroup tasksGroup sync.WaitGroup
externalDomains []string // TODO: i don't like hardcoding external/internal logic here externalDomains []string // TODO: i don't like hardcoding external/internal logic here
internalDomains []string internalDomains []string
@ -37,15 +37,15 @@ func New(state *state.RuntimeState, config config.HostConfig) *Role {
func (r *Role) sendUpdate(domains []string, role types.Role) { func (r *Role) sendUpdate(domains []string, role types.Role) {
state := types.HostState{ state := types.HostState{
Domains: domains, Domains: domains,
Name: r.state.Self.Name, Address: r.config.IpAddress,
Endpoint: r.state.Self.Address, Hostname: r.state.Self.Hostname,
} }
for _, node := range r.state.Registry.ByRole(role) { for _, node := range r.state.Registry.ByRole(role) {
r.tasksGroup.Go(func() { r.tasksGroup.Go(func() {
logger := log.With().Str("name", node.Name).Logger() logger := log.With().Str("name", node.Hostname).Logger()
logger.Debug().Msg("sending update") logger.Debug().Msg("sending update")
if _, err := client.Post[any](node.Address, types.PathDnsCallback, state); err != nil { if _, err := client.Post[any](node.Endpoint, types.PathDnsCallback, state); err != nil {
logger.Warn().Err(err).Msg("unable to send dns info") logger.Warn().Err(err).Msg("unable to send dns info")
} else { } else {
logger.Debug().Msg("update sent") logger.Debug().Msg("update sent")
@ -87,8 +87,8 @@ func (r *Role) onCallback(w http.ResponseWriter, req *http.Request) {
func (r *Role) getInternal() (types.HostState, error) { func (r *Role) getInternal() (types.HostState, error) {
return types.HostState{ return types.HostState{
Domains: r.internalDomains, Domains: r.internalDomains,
Endpoint: r.config.IpAddress, Address: r.config.IpAddress,
Name: r.state.Self.Name, Hostname: r.state.Self.Hostname,
}, nil }, nil
} }

View file

@ -4,11 +4,11 @@ import (
"context" "context"
"sync" "sync"
"git.wzray.com/homelab/mastermind/internal/config" "git.wzray.com/homelab/hivemind/internal/config"
"git.wzray.com/homelab/mastermind/internal/roles" "git.wzray.com/homelab/hivemind/internal/roles"
"git.wzray.com/homelab/mastermind/internal/state" "git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/client" "git.wzray.com/homelab/hivemind/internal/web/client"
) )
type Role struct { type Role struct {
@ -50,7 +50,7 @@ func (r *Role) OnShutdown() error {
func (r *Role) notify(path types.Path, v any) { func (r *Role) notify(path types.Path, v any) {
for _, n := range r.state.Registry.Nodes() { for _, n := range r.state.Registry.Nodes() {
addr := n.Address addr := n.Endpoint
r.tasksGroup.Go(func() { r.tasksGroup.Go(func() {
client.Post[any](addr, path, v) client.Post[any](addr, path, v)
}) })
@ -68,17 +68,17 @@ func (r *Role) onJoin(node types.Node) ([]types.Node, error) {
} }
func (r *Role) onLeave(node types.Node) (bool, error) { func (r *Role) onLeave(node types.Node) (bool, error) {
if err := r.state.Registry.RemoveNode(node.Name); err != nil { if err := r.state.Registry.RemoveNode(node.Hostname); err != nil {
return false, err return false, err
} }
r.notify(types.PathNodeLeave, node.Name) r.notify(types.PathNodeLeave, node)
return true, nil return true, nil
} }
func (r *Role) onKeepAlive(node types.Node) (bool, error) { func (r *Role) onKeepAlive(node types.Node) (bool, error) {
if ok := r.state.Registry.Exists(node.Name); !ok { if ok := r.state.Registry.Exists(node.Hostname); !ok {
_, err := r.onJoin(node) _, err := r.onJoin(node)
return true, err return true, err
} }

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"time" "time"
"git.wzray.com/homelab/mastermind/internal/state" "git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/client" "git.wzray.com/homelab/hivemind/internal/web/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -33,14 +33,14 @@ func newObserver(
func (o *observer) pollNodes(ctx context.Context, onLeave func(types.Node) error) { func (o *observer) pollNodes(ctx context.Context, onLeave func(types.Node) error) {
for _, n := range o.state.Registry.Nodes() { for _, n := range o.state.Registry.Nodes() {
name := n.Name name := n.Hostname
logger := log.With().Str("name", name).Logger() logger := log.With().Str("name", name).Logger()
logger.Debug().Msg("checking node") logger.Debug().Msg("checking node")
delay := time.Duration(o.backoff) delay := time.Duration(o.backoff)
alive := false alive := false
for i := o.backoffCount; i > 0; i-- { for i := o.backoffCount; i > 0; i-- {
_, err := client.Get[any](n.Address, types.PathNodeHealthcheck) _, err := client.Get[any](n.Endpoint, types.PathNodeHealthcheck)
if err == nil { if err == nil {
logger.Debug().Msg("node is alive") logger.Debug().Msg("node is alive")

View file

@ -6,10 +6,10 @@ import (
"sync" "sync"
"time" "time"
"git.wzray.com/homelab/mastermind/internal/config" "git.wzray.com/homelab/hivemind/internal/config"
"git.wzray.com/homelab/mastermind/internal/state" "git.wzray.com/homelab/hivemind/internal/state"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/client" "git.wzray.com/homelab/hivemind/internal/web/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -29,10 +29,10 @@ func New(state *state.RuntimeState, config config.NodeConfig) *Role {
func (r *Role) Join(bootstrap string) error { func (r *Role) Join(bootstrap string) error {
masters := make(map[string]struct{}) masters := make(map[string]struct{})
for _, node := range r.state.Registry.ByRole(types.MasterRole) { for _, node := range r.state.Registry.ByRole(types.MasterRole) {
if node.Name == r.state.Self.Name { if node.Hostname == r.state.Self.Hostname {
continue continue
} }
masters[node.Address] = struct{}{} masters[node.Endpoint] = struct{}{}
} }
if bootstrap != "" { if bootstrap != "" {
masters[bootstrap] = struct{}{} masters[bootstrap] = struct{}{}
@ -69,10 +69,10 @@ func (r *Role) Leave() error {
sent := false sent := false
for _, m := range masters { for _, m := range masters {
logger := log.With().Str("name", m.Name).Logger() logger := log.With().Str("name", m.Hostname).Logger()
logger.Debug().Msg("sending leave message") logger.Debug().Msg("sending leave message")
_, err := client.Post[any](m.Address, types.PathMasterLeave, r.state.Self) _, err := client.Post[any](m.Endpoint, types.PathMasterLeave, r.state.Self)
if err != nil { if err != nil {
logger.Debug().Err(err).Msg("unable to send leave message") logger.Debug().Err(err).Msg("unable to send leave message")
continue continue
@ -114,10 +114,10 @@ func (r *Role) keepaliveFunc(ctx context.Context) func() {
sent := false sent := false
for _, m := range masters { for _, m := range masters {
logger := log.With().Str("name", m.Name).Logger() logger := log.With().Str("name", m.Hostname).Logger()
logger.Debug().Msg("sending keepalive packet") logger.Debug().Msg("sending keepalive packet")
if _, err := client.Post[any](m.Address, types.PathMasterKeepalive, r.state.Self); err != nil { if _, err := client.Post[any](m.Endpoint, types.PathMasterKeepalive, r.state.Self); err != nil {
continue continue
} else { } else {
logger.Debug().Msg("keepalive packet sent") logger.Debug().Msg("keepalive packet sent")
@ -151,7 +151,7 @@ func (r *Role) onJoin(node types.Node) (bool, error) {
} }
func (r *Role) onLeave(node types.Node) (bool, error) { func (r *Role) onLeave(node types.Node) (bool, error) {
if err := r.state.Registry.RemoveNode(node.Name); err != nil { if err := r.state.Registry.RemoveNode(node.Hostname); err != nil {
return false, err return false, err
} }
return true, nil return true, nil

View file

@ -3,7 +3,7 @@ package roles
import ( import (
"context" "context"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
) )
type Role interface { type Role interface {

View file

@ -1,8 +1,8 @@
package state package state
import ( import (
"git.wzray.com/homelab/mastermind/internal/registry" "git.wzray.com/homelab/hivemind/internal/registry"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
) )
type RuntimeState struct { type RuntimeState struct {

View file

@ -2,6 +2,6 @@ package types
type HostState struct { type HostState struct {
Domains []string Domains []string
Endpoint string Address string
Name string Hostname string
} }

View file

@ -1,16 +1,27 @@
package types package types
import "fmt"
// TODO: consider moving this type back to registry // TODO: consider moving this type back to registry
type Node struct { type Node struct {
Address string `json:"address"` Hostname string `json:"hostname"`
Name string `json:"name"` Address string `json:"address"`
Roles []Role `json:"roles"` Port int `json:"port"`
Roles []Role `json:"roles"`
Endpoint string `json:"endpoint"`
} }
func NewNode(address string, name string, roles []Role) Node { func NewNode(
hostname string,
address string,
port int,
roles []Role,
) Node {
return Node{ return Node{
Address: address, Hostname: hostname,
Name: name, Address: address,
Roles: roles, Port: port,
Roles: roles,
Endpoint: fmt.Sprintf("%s:%d", address, port),
} }
} }

View file

@ -1 +0,0 @@
package types

View file

@ -6,6 +6,8 @@ import (
"net/http" "net/http"
) )
// TODO: split this up
type Path string type Path string
func (p Path) String() string { func (p Path) String() string {

View file

@ -10,8 +10,8 @@ import (
"net/url" "net/url"
"time" "time"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/middleware" "git.wzray.com/homelab/hivemind/internal/web/middleware"
) )
type client struct { type client struct {

View file

@ -5,8 +5,8 @@ import (
"io" "io"
"net/http" "net/http"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
"git.wzray.com/homelab/mastermind/internal/web/middleware" "git.wzray.com/homelab/hivemind/internal/web/middleware"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )

View file

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.wzray.com/homelab/mastermind/internal/types" "git.wzray.com/homelab/hivemind/internal/types"
) )
func fail(format string, a ...any) []byte { func fail(format string, a ...any) []byte {