Compare commits
12 commits
30750eebe3
...
f4154ec719
| Author | SHA1 | Date | |
|---|---|---|---|
| f4154ec719 | |||
| fa6174cc7d | |||
| 8c5716dba6 | |||
| 53b526305e | |||
| 6e8cf18be0 | |||
| 836cd1361e | |||
| 40f6c0ad24 | |||
| 7185f55609 | |||
| d3f0628396 | |||
| 29fbf229f5 | |||
| ca3297d866 | |||
| 738fe2fe7d |
9 changed files with 60 additions and 39 deletions
10
Makefile
10
Makefile
|
|
@ -1,13 +1,19 @@
|
||||||
|
|
||||||
all: hivemind hivemind-lite
|
all: hivemind hivemind-musl hivemind-lite
|
||||||
|
|
||||||
hivemind:
|
hivemind:
|
||||||
go build -o build/hivemind ./cmd/hivemind
|
go build -o build/hivemind ./cmd/hivemind
|
||||||
|
|
||||||
|
hivemind-musl:
|
||||||
|
CC=musl-gcc go build \
|
||||||
|
-ldflags="-linkmode external -extldflags '-static'" \
|
||||||
|
-o build/hivemind-musl \
|
||||||
|
./cmd/hivemind
|
||||||
|
|
||||||
hivemind-lite:
|
hivemind-lite:
|
||||||
CC=musl-gcc go build \
|
CC=musl-gcc go build \
|
||||||
-ldflags="-linkmode external -extldflags '-static'" \
|
-ldflags="-linkmode external -extldflags '-static'" \
|
||||||
-o build/hivemind-lite \
|
-o build/hivemind-lite \
|
||||||
./cmd/hivemind-lite
|
./cmd/hivemind-lite
|
||||||
|
|
||||||
.phony: all hivemind hivemind-lite
|
.phony: all hivemind hivemind-musl hivemind-lite
|
||||||
|
|
|
||||||
5
TODO.md
Normal file
5
TODO.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
- nginx role
|
||||||
|
- don't make roles beg to keep them alive, instead wait for a timeout since the last keepalive message and then probe them
|
||||||
|
- think about choosing the master for the keepalive message (should be somewhat load-balanced)
|
||||||
|
- hivemind lite should not just print `hivemind-lite` lol
|
||||||
|
- different transport (maybe something like a custom binary protocol)
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.wzray.com/homelab/hivemind/internal/config"
|
"git.wzray.com/homelab/hivemind/internal/config"
|
||||||
|
|
@ -27,8 +28,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configFile = "/etc/hivemind/config.toml"
|
configFile = "/etc/hivemind/config.toml"
|
||||||
registryFile = "/var/lib/hivemind/registry"
|
registryFile = "/var/lib/hivemind/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func levelToZerolog(l config.LogLevel) zerolog.Level {
|
func levelToZerolog(l config.LogLevel) zerolog.Level {
|
||||||
|
|
@ -64,22 +65,22 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config, err := config.FromFile(configFile)
|
configuration, err := config.FromFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("unable to read config file")
|
log.Fatal().Err(err).Msg("unable to read config file")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.Validate(); err != nil {
|
if err := configuration.Validate(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("invalid configuration")
|
log.Fatal().Err(err).Msg("invalid configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(levelToZerolog(config.Node.LogLevel))
|
zerolog.SetGlobalLevel(levelToZerolog(configuration.Node.LogLevel))
|
||||||
|
|
||||||
self := types.NewNode(
|
self := types.NewNode(
|
||||||
config.Node.Hostname,
|
configuration.Node.Hostname,
|
||||||
config.Node.Address,
|
configuration.Node.Address,
|
||||||
config.Node.Port,
|
configuration.Node.Port,
|
||||||
config.Roles,
|
configuration.Roles,
|
||||||
)
|
)
|
||||||
|
|
||||||
filestore := registry.NewFileStorage(registryFile)
|
filestore := registry.NewFileStorage(registryFile)
|
||||||
|
|
@ -88,29 +89,29 @@ func main() {
|
||||||
|
|
||||||
state := state.New(registry, self)
|
state := state.New(registry, self)
|
||||||
|
|
||||||
nodeRole := node.New(state, config.Node)
|
nodeRole := node.New(state, configuration.Node)
|
||||||
|
|
||||||
var builder middleware.MiddlewareBuilder
|
var builder middleware.MiddlewareBuilder
|
||||||
middlewares := builder.Prepare()
|
middlewares := builder.Prepare()
|
||||||
|
|
||||||
client.Init(middlewares)
|
client.Init(middlewares)
|
||||||
|
|
||||||
listenAddr := fmt.Sprintf("%v:%v", config.Node.ListenOn, config.Node.Port)
|
listenAddr := fmt.Sprintf("%v:%v", configuration.Node.ListenOn, configuration.Node.Port)
|
||||||
server := server.NewServer(listenAddr, middlewares)
|
server := server.NewServer(listenAddr, middlewares)
|
||||||
|
|
||||||
roles := make([]roles.Role, 0)
|
roles := make([]roles.Role, 0)
|
||||||
roles = append(roles, nodeRole)
|
roles = append(roles, nodeRole)
|
||||||
|
|
||||||
for _, role := range config.Roles {
|
for _, role := range configuration.Roles {
|
||||||
switch role {
|
switch role {
|
||||||
case types.MasterRole:
|
case types.MasterRole:
|
||||||
role := master.New(state, config.Configs.Master)
|
role := master.New(state, configuration.Configs.Master)
|
||||||
roles = append(roles, role)
|
roles = append(roles, role)
|
||||||
case types.DnsRole:
|
case types.DnsRole:
|
||||||
role := dns.New(state, config.Configs.Dns)
|
role := dns.New(state, configuration.Configs.Dns)
|
||||||
roles = append(roles, role)
|
roles = append(roles, role)
|
||||||
case types.HostRole:
|
case types.HostRole:
|
||||||
role := host.New(state, config.Configs.Host)
|
role := host.New(state, configuration.Configs.Host)
|
||||||
roles = append(roles, role)
|
roles = append(roles, role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +126,12 @@ func main() {
|
||||||
serverError <- server.Listen()
|
serverError <- server.Listen()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := nodeRole.Join(config.Node.BootstrapMaster); err != nil {
|
bootstrapMaster := configuration.Node.BootstrapMaster
|
||||||
|
if !strings.Contains(bootstrapMaster, ":") {
|
||||||
|
bootstrapMaster += fmt.Sprintf(":%d", config.DefaultConfig.Node.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nodeRole.Join(bootstrapMaster); err != nil {
|
||||||
log.Warn().Err(err).Msg("unable to join")
|
log.Warn().Err(err).Msg("unable to join")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("joined")
|
log.Info().Msg("joined")
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func FromFile(filename string) (Config, error) {
|
||||||
return Config{}, fmt.Errorf("parse config file: %w", err)
|
return Config{}, fmt.Errorf("parse config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := defaultConfig
|
config := DefaultConfig
|
||||||
config.Node.Merge(temp.Node)
|
config.Node.Merge(temp.Node)
|
||||||
|
|
||||||
if c := temp.Configs.Master; c != nil {
|
if c := temp.Configs.Master; c != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
var defaultConfig = Config{
|
var DefaultConfig = Config{
|
||||||
Node: NodeConfig{
|
Node: NodeConfig{
|
||||||
ListenOn: "0.0.0.0",
|
ListenOn: "0.0.0.0",
|
||||||
Port: 56714,
|
Port: 56714,
|
||||||
KeepaliveInterval: 1,
|
KeepaliveInterval: 10,
|
||||||
LogLevel: LogLevelInfo,
|
LogLevel: LogLevelInfo,
|
||||||
},
|
},
|
||||||
Configs: Configs{
|
Configs: Configs{
|
||||||
Master: MasterConfig{
|
Master: MasterConfig{
|
||||||
ObserverInterval: 10,
|
ObserverInterval: 120,
|
||||||
BackoffSeconds: 2,
|
BackoffSeconds: 1,
|
||||||
BackoffCount: 3,
|
BackoffCount: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,12 @@ func (r *Role) getInternal() (types.HostState, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Role) getExternal() (types.HostState, error) {
|
func (r *Role) getExternal() (types.HostState, error) {
|
||||||
return types.HostState{}, nil
|
return types.HostState{
|
||||||
|
Domains: r.externalDomains,
|
||||||
|
Address: r.config.IpAddress,
|
||||||
|
Hostname: r.state.Self.Hostname,
|
||||||
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Role) RegisterHandlers(rg types.Registrator) {
|
func (r *Role) RegisterHandlers(rg types.Registrator) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func (o *observer) pollNodes(ctx context.Context, onLeave func(types.Node) error
|
||||||
|
|
||||||
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 - 1; i >= 0; i-- {
|
||||||
_, err := client.Get[any](n.Endpoint, types.PathNodeHealthcheck)
|
_, err := client.Get[any](n.Endpoint, types.PathNodeHealthcheck)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package node
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -14,14 +15,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Role struct {
|
type Role struct {
|
||||||
state *state.RuntimeState
|
state *state.RuntimeState
|
||||||
keepaliveGroup sync.WaitGroup
|
keepaliveGroup sync.WaitGroup
|
||||||
config config.NodeConfig
|
config config.NodeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(state *state.RuntimeState, config config.NodeConfig) *Role {
|
func New(state *state.RuntimeState, config config.NodeConfig) *Role {
|
||||||
return &Role{
|
return &Role{
|
||||||
state: state,
|
state: state,
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,25 +41,28 @@ func (r *Role) Join(bootstrap string) error {
|
||||||
return errors.New("no masters configured")
|
return errors.New("no masters configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
for m := range masters {
|
for m := range masters {
|
||||||
logger := log.With().Str("host", m).Logger()
|
logger := log.With().Str("host", m).Logger()
|
||||||
logger.Debug().Msg("trying to join via master")
|
logger.Debug().Msg("trying to join via master")
|
||||||
|
|
||||||
nodes, err := client.Post[[]types.Node](m, "/master/join", r.state.Self)
|
nodes, err := client.Post[[]types.Node](m, "/master/join", r.state.Self)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
logger.Debug().Err(err).Msg("unable to join")
|
logger.Debug().Err(err).Msg("unable to join")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.state.Registry.Set(*nodes); err != nil {
|
if err := r.state.Registry.Set(*nodes); err != nil {
|
||||||
logger.Debug().Err(err).Msg("unable to set master's nodes")
|
logger.Debug().Err(err).Msg("unable to set master's nodes")
|
||||||
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("unable to join")
|
return fmt.Errorf("unable to join with any master: %w", errors.Join(errs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Role) Leave() error {
|
func (r *Role) Leave() error {
|
||||||
|
|
@ -67,7 +71,7 @@ func (r *Role) Leave() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sent := false
|
var errs []error
|
||||||
for _, m := range masters {
|
for _, m := range masters {
|
||||||
logger := log.With().Str("name", m.Hostname).Logger()
|
logger := log.With().Str("name", m.Hostname).Logger()
|
||||||
logger.Debug().Msg("sending leave message")
|
logger.Debug().Msg("sending leave message")
|
||||||
|
|
@ -75,19 +79,15 @@ func (r *Role) Leave() error {
|
||||||
_, err := client.Post[any](m.Endpoint, 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")
|
||||||
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
sent = true
|
|
||||||
logger.Debug().Msg("leave message sent")
|
logger.Debug().Msg("leave message sent")
|
||||||
break
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sent {
|
return fmt.Errorf("unable to send leave message to any master: %w", errors.Join(errs...))
|
||||||
return errors.New("unable to send leave message")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Role) OnStartup(ctx context.Context) error {
|
func (r *Role) OnStartup(ctx context.Context) error {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"last_update":1768661288311,"nodes":{"ext.zvray.ru":{"address":"10.160.0.1:56714","name":"ext.zvray.ru","roles":["dns"]},"laptop.spb.wzray.com":{"address":"laptop.spb.wzray.com:56714","name":"laptop.spb.wzray.com","roles":["master","dns","host"]}}}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue