// Package registry contains the static project, machine, and herdr topology. package registry import ( "encoding/json" "errors" "fmt" "net" "os" "sort" "strings" "time" ) var ( ErrUnknownProject = errors.New("unknown project") ErrUnknownMachine = errors.New("unknown machine") ErrNoAffinity = errors.New("project has no machine affinity") ) type Project struct { ID string `json:"id"` MachineAffinity []string `json:"machine_affinity"` } type Machine struct { ID string `json:"id"` Address string `json:"address"` } type Herdr struct { ID string `json:"id"` MachineID string `json:"machine_id"` Address string `json:"address,omitempty"` Harness string `json:"harness,omitempty"` Protocol string `json:"protocol,omitempty"` Capabilities []string `json:"capabilities"` Concurrency int `json:"concurrency"` QuotaLimit float64 `json:"quota_limit,omitempty"` } type Config struct { Projects []Project `json:"projects"` Machines []Machine `json:"machines"` Herdrs []Herdr `json:"herdrs"` } type Registry struct { projects map[string]Project machines map[string]Machine herdrs map[string]Herdr } func Load(path string) (Registry, error) { b, err := os.ReadFile(path) if err != nil { return Registry{}, err } var c Config if err = json.Unmarshal(b, &c); err != nil { return Registry{}, fmt.Errorf("registry config: %w", err) } return New(c) } func New(c Config) (Registry, error) { r := Registry{map[string]Project{}, map[string]Machine{}, map[string]Herdr{}} for _, p := range c.Projects { if err := putID(r.projects, p.ID, "project"); err != nil { return Registry{}, err } if len(p.MachineAffinity) == 0 { return Registry{}, fmt.Errorf("project %q: %w", p.ID, ErrNoAffinity) } r.projects[p.ID] = p } for _, m := range c.Machines { if err := putID(r.machines, m.ID, "machine"); err != nil { return Registry{}, err } if strings.TrimSpace(m.Address) == "" { return Registry{}, fmt.Errorf("machine %q: address required", m.ID) } r.machines[m.ID] = m } for _, h := range c.Herdrs { if err := putID(r.herdrs, h.ID, "herdr"); err != nil { return Registry{}, err } if _, ok := r.machines[h.MachineID]; !ok { return Registry{}, fmt.Errorf("herdr %q: %w %q", h.ID, ErrUnknownMachine, h.MachineID) } if h.Concurrency < 0 { return Registry{}, fmt.Errorf("herdr %q: negative concurrency", h.ID) } r.herdrs[h.ID] = h } for _, p := range r.projects { for _, m := range p.MachineAffinity { if _, ok := r.machines[m]; !ok { return Registry{}, fmt.Errorf("project %q: %w %q", p.ID, ErrUnknownMachine, m) } } } return r, nil } func putID[T any](m map[string]T, id, kind string) error { if strings.TrimSpace(id) == "" { return fmt.Errorf("%s id required", kind) } if _, ok := m[id]; ok { return fmt.Errorf("duplicate %s %q", kind, id) } return nil } func (r Registry) Project(id string) (Project, bool) { p, ok := r.projects[id]; return p, ok } func (r Registry) Machine(id string) (Machine, bool) { m, ok := r.machines[id]; return m, ok } func (r Registry) Herdr(id string) (Herdr, bool) { h, ok := r.herdrs[id]; return h, ok } func (r Registry) Herdrs() []Herdr { out := make([]Herdr, 0, len(r.herdrs)) for _, h := range r.herdrs { out = append(out, h) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } func (r Registry) Projects() []Project { return projects(r.projects) } func projects(m map[string]Project) []Project { out := make([]Project, 0, len(m)) for _, v := range m { out = append(out, v) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } type Reachability interface { Reachable(address string, timeout time.Duration) bool } type TCPReachability struct{} func (TCPReachability) Reachable(address string, timeout time.Duration) bool { c, err := net.DialTimeout("tcp", address, timeout) if err != nil { return false } _ = c.Close() return true } func (r Registry) Candidates(project string, check Reachability, timeout time.Duration) ([]Herdr, error) { p, ok := r.projects[project] if !ok { return nil, ErrUnknownProject } allowed := map[string]bool{} for _, m := range p.MachineAffinity { allowed[m] = true } out := []Herdr{} for _, h := range r.herdrs { if !allowed[h.MachineID] { continue } addr := h.Address if addr == "" { addr = r.machines[h.MachineID].Address } if check == nil || check.Reachable(addr, timeout) { out = append(out, h) } } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out, nil }