diff --git a/Makefile b/Makefile index 9ad8070..71e9610 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,19 @@ -all: hivemind hivemind-lite +all: hivemind hivemind-musl hivemind-lite 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: CC=musl-gcc go build \ -ldflags="-linkmode external -extldflags '-static'" \ -o build/hivemind-lite \ ./cmd/hivemind-lite -.phony: all hivemind hivemind-lite +.phony: all hivemind hivemind-musl hivemind-lite diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..27f744a --- /dev/null +++ b/TODO.md @@ -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) diff --git a/cmd/hivemind/main.go b/cmd/hivemind/main.go index 7496017..c614986 100644 --- a/cmd/hivemind/main.go +++ b/cmd/hivemind/main.go @@ -7,6 +7,7 @@ import ( "os/signal" "path/filepath" "strconv" + "strings" "syscall" "git.wzray.com/homelab/hivemind/internal/config" @@ -27,8 +28,8 @@ import ( ) var ( - configFile = "/etc/hivemind/config.toml" - registryFile = "/var/lib/hivemind/registry" + configFile = "/etc/hivemind/config.toml" + registryFile = "/var/lib/hivemind/registry" ) func levelToZerolog(l config.LogLevel) zerolog.Level { @@ -64,22 +65,22 @@ func init() { } func main() { - config, err := config.FromFile(configFile) + configuration, err := config.FromFile(configFile) if err != nil { 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") } - zerolog.SetGlobalLevel(levelToZerolog(config.Node.LogLevel)) + zerolog.SetGlobalLevel(levelToZerolog(configuration.Node.LogLevel)) self := types.NewNode( - config.Node.Hostname, - config.Node.Address, - config.Node.Port, - config.Roles, + configuration.Node.Hostname, + configuration.Node.Address, + configuration.Node.Port, + configuration.Roles, ) filestore := registry.NewFileStorage(registryFile) @@ -88,29 +89,29 @@ func main() { state := state.New(registry, self) - nodeRole := node.New(state, config.Node) + nodeRole := node.New(state, configuration.Node) var builder middleware.MiddlewareBuilder middlewares := builder.Prepare() 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) roles := make([]roles.Role, 0) roles = append(roles, nodeRole) - for _, role := range config.Roles { + for _, role := range configuration.Roles { switch role { case types.MasterRole: - role := master.New(state, config.Configs.Master) + role := master.New(state, configuration.Configs.Master) roles = append(roles, role) case types.DnsRole: - role := dns.New(state, config.Configs.Dns) + role := dns.New(state, configuration.Configs.Dns) roles = append(roles, role) case types.HostRole: - role := host.New(state, config.Configs.Host) + role := host.New(state, configuration.Configs.Host) roles = append(roles, role) } } @@ -125,7 +126,12 @@ func main() { 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") } else { log.Info().Msg("joined") diff --git a/internal/config/config.go b/internal/config/config.go index 25ee12d..2a3a76f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,7 +40,7 @@ func FromFile(filename string) (Config, error) { return Config{}, fmt.Errorf("parse config file: %w", err) } - config := defaultConfig + config := DefaultConfig config.Node.Merge(temp.Node) if c := temp.Configs.Master; c != nil { diff --git a/internal/config/defaults.go b/internal/config/defaults.go index c788047..d0c2a30 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -1,17 +1,17 @@ package config -var defaultConfig = Config{ +var DefaultConfig = Config{ Node: NodeConfig{ ListenOn: "0.0.0.0", Port: 56714, - KeepaliveInterval: 1, + KeepaliveInterval: 10, LogLevel: LogLevelInfo, }, Configs: Configs{ Master: MasterConfig{ - ObserverInterval: 10, - BackoffSeconds: 2, - BackoffCount: 3, + ObserverInterval: 120, + BackoffSeconds: 1, + BackoffCount: 4, }, }, } diff --git a/internal/roles/host/host.go b/internal/roles/host/host.go index 2b35181..ca61e2f 100644 --- a/internal/roles/host/host.go +++ b/internal/roles/host/host.go @@ -93,7 +93,12 @@ func (r *Role) getInternal() (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) { diff --git a/internal/roles/master/observer.go b/internal/roles/master/observer.go index 64f69c8..46153b2 100644 --- a/internal/roles/master/observer.go +++ b/internal/roles/master/observer.go @@ -39,7 +39,7 @@ func (o *observer) pollNodes(ctx context.Context, onLeave func(types.Node) error delay := time.Duration(o.backoff) 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) if err == nil { diff --git a/internal/roles/node/node.go b/internal/roles/node/node.go index 762ae4b..5bc113e 100644 --- a/internal/roles/node/node.go +++ b/internal/roles/node/node.go @@ -3,6 +3,7 @@ package node import ( "context" "errors" + "fmt" "sync" "time" @@ -14,14 +15,14 @@ import ( ) type Role struct { - state *state.RuntimeState + state *state.RuntimeState keepaliveGroup sync.WaitGroup - config config.NodeConfig + config config.NodeConfig } func New(state *state.RuntimeState, config config.NodeConfig) *Role { return &Role{ - state: state, + state: state, config: config, } } @@ -40,25 +41,28 @@ func (r *Role) Join(bootstrap string) error { return errors.New("no masters configured") } + var errs []error for m := range masters { logger := log.With().Str("host", m).Logger() logger.Debug().Msg("trying to join via master") nodes, err := client.Post[[]types.Node](m, "/master/join", r.state.Self) if err != nil { + errs = append(errs, err) logger.Debug().Err(err).Msg("unable to join") continue } if err := r.state.Registry.Set(*nodes); err != nil { logger.Debug().Err(err).Msg("unable to set master's nodes") + errs = append(errs, err) continue } 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 { @@ -67,7 +71,7 @@ func (r *Role) Leave() error { return nil } - sent := false + var errs []error for _, m := range masters { logger := log.With().Str("name", m.Hostname).Logger() 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) if err != nil { logger.Debug().Err(err).Msg("unable to send leave message") + errs = append(errs, err) continue } else { - sent = true logger.Debug().Msg("leave message sent") - break + return nil } } - if !sent { - return errors.New("unable to send leave message") - } - - return nil + return fmt.Errorf("unable to send leave message to any master: %w", errors.Join(errs...)) } func (r *Role) OnStartup(ctx context.Context) error { diff --git a/registry.json b/registry.json deleted file mode 100644 index 7f0cda4..0000000 --- a/registry.json +++ /dev/null @@ -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"]}}} \ No newline at end of file