feat: initial release
This commit is contained in:
parent
a3cf21f5bd
commit
761174d035
41 changed files with 2008 additions and 217 deletions
68
internal/registry/filestorage.go
Normal file
68
internal/registry/filestorage.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.wzray.com/homelab/mastermind/internal/types"
|
||||
)
|
||||
|
||||
type FileStorage struct {
|
||||
filename string
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewFileStorage(filename string) *FileStorage {
|
||||
return &FileStorage{
|
||||
filename: filename,
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FileStorage) EnsureExists() {
|
||||
dirname := path.Dir(fs.filename)
|
||||
if _, err := os.Stat(dirname); os.IsNotExist(err) {
|
||||
os.MkdirAll(dirname, 0755)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fs.filename); os.IsNotExist(err) {
|
||||
fs.Save(&storedConfig{
|
||||
LastUpdate: time.Now().UnixMilli(),
|
||||
Nodes: make(map[string]types.Node),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FileStorage) Save(cfg *storedConfig) error {
|
||||
fs.lock.Lock()
|
||||
defer fs.lock.Unlock()
|
||||
|
||||
buf, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal registry: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fs.filename, buf, 0644); err != nil {
|
||||
return fmt.Errorf("write registry file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FileStorage) Load(cfg *storedConfig) error {
|
||||
fs.lock.Lock()
|
||||
defer fs.lock.Unlock()
|
||||
|
||||
data, err := os.ReadFile(fs.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read registry file: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return fmt.Errorf("unmarshal registry: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
153
internal/registry/registry.go
Normal file
153
internal/registry/registry.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.wzray.com/homelab/mastermind/internal/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
LastUpdate time.Time
|
||||
nodes map[string]types.Node
|
||||
storage Storage
|
||||
lock sync.RWMutex
|
||||
self types.Node
|
||||
observers []chan<- []types.Node
|
||||
}
|
||||
|
||||
func New(storage Storage, self types.Node) *Registry {
|
||||
r := &Registry{
|
||||
storage: storage,
|
||||
nodes: make(map[string]types.Node),
|
||||
self: self,
|
||||
}
|
||||
|
||||
var storedData storedConfig
|
||||
if err := storage.Load(&storedData); err != nil {
|
||||
log.Warn().Err(err).Msg("unable to load registry from storage")
|
||||
goto ret
|
||||
}
|
||||
|
||||
r.LastUpdate = time.UnixMilli(storedData.LastUpdate)
|
||||
r.nodes = storedData.Nodes
|
||||
|
||||
ret:
|
||||
r.nodes[self.Name] = self
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Registry) snapshot() *storedConfig {
|
||||
return &storedConfig{
|
||||
LastUpdate: r.LastUpdate.UnixMilli(),
|
||||
Nodes: maps.Clone(r.nodes),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) notify() {
|
||||
nodes := r.Nodes()
|
||||
for _, c := range r.observers {
|
||||
c <- nodes
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) AllNodes() []types.Node {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
nodes := make([]types.Node, 0, len(r.nodes))
|
||||
for _, n := range r.nodes {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (r *Registry) Nodes() []types.Node {
|
||||
nodes := r.AllNodes()
|
||||
nodes = slices.DeleteFunc(nodes, func(n types.Node) bool {
|
||||
return n.Name == r.self.Name
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (r *Registry) ByRole(role types.Role) []types.Node {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
|
||||
o := make([]types.Node, 0, len(r.nodes))
|
||||
for _, node := range r.nodes {
|
||||
if slices.Contains(node.Roles, role) && node.Name != r.self.Name {
|
||||
o = append(o, node)
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (r *Registry) AddNode(node types.Node) error {
|
||||
r.lock.Lock()
|
||||
r.nodes[node.Name] = node
|
||||
r.LastUpdate = time.Now()
|
||||
snapshot := r.snapshot()
|
||||
r.lock.Unlock()
|
||||
|
||||
if err := r.storage.Save(snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) RemoveNode(nodeName string) error {
|
||||
r.lock.Lock()
|
||||
delete(r.nodes, nodeName)
|
||||
r.LastUpdate = time.Now()
|
||||
snapshot := r.snapshot()
|
||||
r.lock.Unlock()
|
||||
|
||||
if err := r.storage.Save(snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.notify()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) Set(nodes []types.Node) error {
|
||||
r.lock.Lock()
|
||||
r.nodes = make(map[string]types.Node)
|
||||
for _, n := range nodes {
|
||||
r.nodes[n.Name] = n
|
||||
}
|
||||
snapshot := r.snapshot()
|
||||
r.lock.Unlock()
|
||||
|
||||
if err := r.storage.Save(snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.notify()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) Exists(name string) bool {
|
||||
_, ok := r.nodes[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *Registry) OnChanged() <-chan []types.Node { // TODO: rename this
|
||||
c := make(chan []types.Node, 1)
|
||||
r.observers = append(r.observers, c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *Registry) Save() {
|
||||
r.lock.RLock()
|
||||
snapshot := r.snapshot()
|
||||
r.lock.RUnlock()
|
||||
|
||||
r.storage.Save(snapshot)
|
||||
}
|
||||
13
internal/registry/storage.go
Normal file
13
internal/registry/storage.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package registry
|
||||
|
||||
import "git.wzray.com/homelab/mastermind/internal/types"
|
||||
|
||||
type Storage interface {
|
||||
Save(*storedConfig) error
|
||||
Load(*storedConfig) error
|
||||
}
|
||||
|
||||
type storedConfig struct {
|
||||
LastUpdate int64 `json:"last_update"`
|
||||
Nodes map[string]types.Node `json:"nodes"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue