diff --git a/.gitignore b/.gitignore index 42014fc..84c048a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /build/ -*_gen.go diff --git a/Makefile b/Makefile index a18b22c..71e9610 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,19 @@ -all: hivemind hivemind-musl -codegen: - go generate ./... +all: hivemind hivemind-musl hivemind-lite -hivemind: codegen +hivemind: go build -o build/hivemind ./cmd/hivemind -hivemind-musl: codegen +hivemind-musl: CC=musl-gcc go build \ -ldflags="-linkmode external -extldflags '-static'" \ -o build/hivemind-musl \ ./cmd/hivemind -.phony: all codegen hivemind hivemind-musl +hivemind-lite: + CC=musl-gcc go build \ + -ldflags="-linkmode external -extldflags '-static'" \ + -o build/hivemind-lite \ + ./cmd/hivemind-lite + +.phony: all hivemind hivemind-musl hivemind-lite diff --git a/go.mod b/go.mod index 81f11a3..53b5b0a 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,13 @@ module git.wzray.com/homelab/hivemind go 1.25.5 require ( - github.com/BurntSushi/toml v1.6.0 github.com/rs/zerolog v1.34.0 - golang.org/x/tools v0.41.0 + github.com/BurntSushi/toml v1.6.0 ) require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.39.0 // indirect ) diff --git a/go.sum b/go.sum index a04e00f..8a3e287 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -16,14 +14,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/internal/codegen/roleconfig/main.go b/internal/codegen/roleconfig/main.go deleted file mode 100644 index 5d1a485..0000000 --- a/internal/codegen/roleconfig/main.go +++ /dev/null @@ -1,196 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "fmt" - "go/ast" - "go/token" - "go/types" - "io" - "os" - "reflect" - "strings" - - "golang.org/x/tools/go/packages" - "golang.org/x/tools/imports" -) - -const ( - defaultMerge = ` - if s.%[1]s != %[2]s { - s.%[1]s = other.%[1]s - } - ` - defaultValidator = ` - if s.%[1]s == %[2]s { - return errors.New("missing %[3]s") - } - ` - ipValidator = ` - if net.ParseIP(s.%[1]s) == nil { - return fmt.Errorf("invalid %[3]s: %%q", s.%[1]s) - } - ` - positiveValidator = ` - if s.%[1]s < 1 { - return fmt.Errorf("invalid %[3]s: %%q", s.%[1]s) - } - ` -) - -var ( - structName string - packageName string - fileName string -) - -type Validator struct { - code string -} - -type Field struct { - name string - prettyName string - defaultValue string - validators []string -} - -func NewField(name, prettyName, defaultValue, tag string) *Field { - validators := make([]string, 0, 2) - - switch tag { - case "": - validators = append(validators, defaultValidator) - case "ip": - validators = append(validators, defaultValidator, ipValidator) - case "positive": - validators = append(validators, positiveValidator) - case "no": - break - default: - panic(fmt.Sprintf("invalid tag %v", tag)) - } - - return &Field{ - name: name, - prettyName: prettyName, - defaultValue: defaultValue, - validators: validators, - } -} - -func init() { - namePtr := flag.String("name", "", "name of the struct") - flag.Parse() - if namePtr == nil || *namePtr == "" { - fmt.Fprintln(os.Stderr, "Invalid name") - os.Exit(1) - } - structName = *namePtr - - packageName = os.Getenv("GOPACKAGE") - - fileName = fmt.Sprintf("%s_gen.go", strings.TrimSuffix(os.Getenv("GOFILE"), ".go")) -} - -func defaultValue(t types.Type) string { - b, ok := t.Underlying().(*types.Basic) - if !ok { - return "nil" - } - - switch b.Kind() { - case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, - types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64: - return "0" - case types.String: - return `""` - case types.Bool: - return "false" - default: - return "nil" - } -} - -type Package struct { - fields []*Field - imports map[string]struct{} -} - -func enumerateFields(root *types.Struct) []*Field { - fields := make([]*Field, 0, 16) - - for i := range root.NumFields() { - field := root.Field(i) - tag := reflect.StructTag(root.Tag(i)) - - extraValidators := tag.Get("gen") - prettyName := tag.Get("toml") - if embed, ok := field.Type().Underlying().(*types.Struct); ok { - fields = append(fields, enumerateFields(embed)...) - } else { - fields = append(fields, NewField(field.Name(), prettyName, defaultValue(field.Type()), extraValidators)) - } - } - - return fields -} - -func writeMerge(w io.Writer, name string, fields []*Field) { - fmt.Fprintf(w, "func (s *%[1]s) Merge(other %[1]s) {", name) - for _, f := range fields { - fmt.Fprintf(w, defaultMerge, f.name, f.defaultValue) - } - fmt.Fprintf(w, "}\n\n") -} - -func writeValidators(w io.Writer, name string, fields []*Field) { - fmt.Fprintf(w, "func (s %s) Validate() error {", name) - for _, f := range fields { - for _, v := range f.validators { - fmt.Fprintf(w, v, f.name, f.defaultValue, f.prettyName) - } - } - fmt.Fprintf(w, "\nreturn nil\n}\n\n") -} - -func main() { - cfg := &packages.Config{ - Mode: packages.NeedName | - packages.NeedTypes | - packages.NeedTypesInfo | - packages.NeedSyntax | - packages.NeedFiles, - } - pkgs, _ := packages.Load(cfg, ".") - config := *pkgs[0] - for _, v := range config.Syntax { - ast.Inspect(v, func(node ast.Node) bool { - decl, ok := node.(*ast.GenDecl) - if !ok || decl.Tok != token.TYPE { - return true - } - - for _, spec := range decl.Specs { - ts := spec.(*ast.TypeSpec) - if ts.Name.String() != structName { - continue - } - - t := config.TypesInfo.TypeOf(ts.Type.(*ast.StructType)).(*types.Struct) - fields := enumerateFields(t) - - buf := bytes.NewBuffer(make([]byte, 0)) - fmt.Fprintf(buf, "// Code generated by roleconfig; DO NOT EDIT.\n\npackage %s\n", packageName) - writeValidators(buf, structName, fields) - writeMerge(buf, structName, fields) - x, _ := imports.Process(fileName, buf.Bytes(), nil) - os.WriteFile(fileName, x, 0644) - - break - } - return false - }) - } -} diff --git a/internal/config/dns.go b/internal/config/dns.go index 664dcbc..88bc735 100644 --- a/internal/config/dns.go +++ b/internal/config/dns.go @@ -1,8 +1,20 @@ package config -//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig -//go:generate roleconfig -name DnsConfig type DnsConfig struct { - UseSystemd bool `toml:"use_systemd" gen:"no"` + UseSystemd bool `toml:"use_systemd"` baseRoleConfig } + +func (c DnsConfig) Validate() error { + return nil +} + +func (c *DnsConfig) Merge(other DnsConfig) { + if other.set { + c.set = other.set + } + + if other.UseSystemd { + c.UseSystemd = other.UseSystemd + } +} diff --git a/internal/config/host.go b/internal/config/host.go index ac7aeb7..9a302ef 100644 --- a/internal/config/host.go +++ b/internal/config/host.go @@ -1,13 +1,79 @@ package config -//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig -//go:generate roleconfig -name HostConfig +import ( + "errors" + "fmt" + "net" +) + type HostConfig struct { Domain string `toml:"domain"` - IpAddress string `toml:"ip" gen:"ip"` + IpAddress string `toml:"ip"` LocalAddress string `toml:"local_address"` InternalEntrypoint string `toml:"internal_entrypoint"` ExternalEntrypoint string `toml:"external_entrypoint"` ListenAddress string `toml:"listen_address"` baseRoleConfig } + +func (c HostConfig) Validate() error { + if c.Domain == "" { + return errors.New("missing domain") + } + + if c.IpAddress == "" { + return errors.New("missing ip") + } + + if net.ParseIP(c.IpAddress) == nil { + return fmt.Errorf("invalid ip: %q", c.IpAddress) + } + + if c.LocalAddress == "" { + return errors.New("missing local_address") + } + + if c.InternalEntrypoint == "" { + return errors.New("missing internal_entrypoint") + } + + if c.ExternalEntrypoint == "" { + return errors.New("missing external_entrypoint") + } + + if c.ListenAddress == "" { + return errors.New("missing listen_address") + } + + return nil +} + +func (c *HostConfig) Merge(other HostConfig) { + if other.set { + c.set = other.set + } + + if other.Domain != "" { + c.Domain = other.Domain + } + + if other.IpAddress != "" { + c.IpAddress = other.IpAddress + } + + if other.LocalAddress != "" { + c.LocalAddress = other.LocalAddress + } + + if other.InternalEntrypoint != "" { + c.InternalEntrypoint = other.InternalEntrypoint + } + + if other.ExternalEntrypoint != "" { + c.ExternalEntrypoint = other.ExternalEntrypoint + } + + if other.ListenAddress != "" { + c.ListenAddress = other.ListenAddress + } +} diff --git a/internal/config/master.go b/internal/config/master.go index 09d7cf3..fb4b266 100644 --- a/internal/config/master.go +++ b/internal/config/master.go @@ -1,12 +1,54 @@ package config -//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig -//go:generate roleconfig -name MasterConfig +import "errors" + type MasterConfig struct { - ObserverInterval int `toml:"observer_interval" gen:"positive"` - BackoffSeconds int `toml:"backoff_seconds" gen:"positive"` - BackoffCount int `toml:"backoff_count" gen:"positive"` - NodeTimeout int `toml:"node_timeout" gen:"positive"` + ObserverInterval int `toml:"observer_interval"` + BackoffSeconds int `toml:"backoff_seconds"` + BackoffCount int `toml:"backoff_count"` + NodeTimeout int `toml:"node_timeout"` baseRoleConfig } + +func (c MasterConfig) Validate() error { + if c.ObserverInterval < 1 { + return errors.New("invalid observer_interval") + } + + if c.BackoffSeconds < 1 { + return errors.New("invalid backoff_seconds") + } + + if c.BackoffCount < 1 { + return errors.New("invalid backoff_count") + } + + if c.NodeTimeout < 1 { + return errors.New("invalid node_timeout") + } + + return nil +} + +func (c *MasterConfig) Merge(other MasterConfig) { + if other.set { + c.set = true + } + + if other.ObserverInterval != 0 { + c.ObserverInterval = other.ObserverInterval + } + + if other.BackoffSeconds != 0 { + c.BackoffSeconds = other.BackoffSeconds + } + + if other.BackoffCount != 0 { + c.BackoffCount = other.BackoffCount + } + + if other.NodeTimeout != 0 { + c.NodeTimeout = other.NodeTimeout + } +} diff --git a/internal/config/node.go b/internal/config/node.go index 318742b..0ee3da0 100644 --- a/internal/config/node.go +++ b/internal/config/node.go @@ -1,7 +1,9 @@ package config import ( + "errors" "fmt" + "net" "strings" ) @@ -26,16 +28,68 @@ func (l *LogLevel) UnmarshalText(data []byte) error { } } -//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig -//go:generate roleconfig -name NodeConfig type NodeConfig struct { Hostname string `toml:"hostname"` Address string `toml:"address"` Port int `toml:"port"` - KeepaliveInterval int `toml:"keepalive_interval" gen:"positive"` + KeepaliveInterval int `toml:"keepalive_interval"` LogLevel LogLevel `toml:"log_level"` - BootstrapMaster string `toml:"bootstrap_master" gen:"no"` - ListenOn string `toml:"listen_on" gen:"ip"` + BootstrapMaster string `toml:"bootstrap_master"` + ListenOn string `toml:"listen_on"` +} + +func (c NodeConfig) Validate() error { + if c.Address == "" { + return errors.New("missing address") + } + + if c.Hostname == "" { + return errors.New("missing hostname") + } + + if c.KeepaliveInterval < 1 { + return errors.New("invalid keepalive_interval") + } + + if c.ListenOn == "" { + return errors.New("missing listen_on") + } + + if net.ParseIP(c.ListenOn) == nil { + return fmt.Errorf("invalid listen_on: %v", c.ListenOn) + } + + return nil +} + +func (c *NodeConfig) Merge(other NodeConfig) { + if other.Hostname != "" { + c.Hostname = other.Hostname + } + + if other.Address != "" { + c.Address = other.Address + } + + if other.BootstrapMaster != "" { + c.BootstrapMaster = other.BootstrapMaster + } + + if other.ListenOn != "" { + c.ListenOn = other.ListenOn + } + + if other.Port != 0 { + c.Port = other.Port + } + + if other.KeepaliveInterval != 0 { + c.KeepaliveInterval = other.KeepaliveInterval + } + + if other.LogLevel != "" { + c.LogLevel = other.LogLevel + } } diff --git a/internal/config/role.go b/internal/config/role.go index 7928542..4d7069d 100644 --- a/internal/config/role.go +++ b/internal/config/role.go @@ -1,5 +1,5 @@ package config type baseRoleConfig struct { - set bool `gen:"no"` + set bool }