258 lines
9.0 KiB
Go
258 lines
9.0 KiB
Go
package main
|
|
|
|
// Config's records out of the controller (novox/hq ADR 0565 §3, §15, and the progressive insight of 2026-10-29):
|
|
// mesh-config keeps each placed module's declarations in its own state `declared`, reads the list of predefined keys
|
|
// from the indexer's `predefined` and a pull request's config from the indexer's `config-candidates`.
|
|
//
|
|
// **Its own sources hold always** (ADR 0590, issue 1126): the controller ended by its unassignment, so its pull-declares,
|
|
// the take-over that switched away from them, compare and hand-back are gone, and the switch is the indexer's whatever
|
|
// a key once said.
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"time"
|
|
)
|
|
|
|
// The states and reads.
|
|
const (
|
|
// DeclaredState is each placed module's declarations at the trunk, one key per module (ADR 0565 §3).
|
|
DeclaredState = "declared"
|
|
|
|
// IndexerPredefinedRead and CandidatesRead are the indexer's states (ADR 0565 §2).
|
|
IndexerPredefinedRead = indexerModule + "." + catalog.StatePredefined
|
|
CandidatesRead = indexerModule + "." + catalog.StateConfigCandidates
|
|
)
|
|
|
|
// SourceIndexer is the one source of a pull request's config and of the predefined list: the indexer's.
|
|
const SourceIndexer = "indexer"
|
|
|
|
// Switch is which source holds: the indexer's, always, since the controller ended (ADR 0590).
|
|
type Switch struct {
|
|
Source string `json:"source"`
|
|
Why string `json:"why"`
|
|
}
|
|
|
|
// source is the source that holds: the indexer's.
|
|
func (c *Config) source() (Switch, error) {
|
|
return Switch{Source: SourceIndexer, Why: "config reads its own sources since the controller ended by its unassignment (novox/hq ADR 0590)"}, nil
|
|
}
|
|
|
|
// ---- declared --------------------------------------------------------------------------------------------
|
|
|
|
// DeclaredKept is one key of `declared`: a module's declarations as the manifest the indexer's `modules.<module>`
|
|
// names holds them, with that manifest's digest and commit (ADR 0565 §3).
|
|
type DeclaredKept struct {
|
|
Module string `json:"module"`
|
|
Manifest string `json:"manifest"`
|
|
Commit string `json:"commit,omitempty"`
|
|
Config map[string]Declaration `json:"settings"`
|
|
}
|
|
|
|
// trunkKept is a module's declarations at the trunk with the manifest they come from.
|
|
func (c *Config) trunkKept(module string) (DeclaredKept, error) {
|
|
key, err := catalogclient.ModulesKey(module)
|
|
if err != nil {
|
|
return DeclaredKept{}, err
|
|
}
|
|
e, err := c.Store.Get(ModulesRead, key)
|
|
if err != nil || e == nil {
|
|
return DeclaredKept{}, fmt.Errorf("the indexer's modules.%s cannot be read (%v)", module, err)
|
|
}
|
|
var m struct {
|
|
Manifest string `json:"manifest"`
|
|
Commit string `json:"commit"`
|
|
}
|
|
if err := json.Unmarshal(e.Value, &m); err != nil || m.Manifest == "" {
|
|
return DeclaredKept{}, fmt.Errorf("the indexer's modules.%s names no manifest", module)
|
|
}
|
|
d, err := c.manifestDeclarations(module, m.Manifest)
|
|
if err != nil {
|
|
return DeclaredKept{}, err
|
|
}
|
|
return DeclaredKept{Module: module, Manifest: m.Manifest, Commit: m.Commit, Config: d.Config}, nil
|
|
}
|
|
|
|
// writeDeclared makes `declared` hold each placed module's trunk declarations, writing a key only when it differs
|
|
// (ADR 0289 §1) and removing one of a module placed nowhere. It answers the modules written.
|
|
func (c *Config) writeDeclared(s Sources) ([]string, error) {
|
|
held := map[string][]byte{}
|
|
if err := c.each(DeclaredState, func(key string, raw []byte) error {
|
|
held[key] = raw
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
var written []string
|
|
var problems []error
|
|
placed := map[string]bool{}
|
|
for _, module := range modulesOf(s.Assignments) {
|
|
placed[module] = true
|
|
k, err := c.trunkKept(module)
|
|
if err != nil {
|
|
continue // unresolved, said by the resolution; its key stays as it was
|
|
}
|
|
raw, _ := json.Marshal(k)
|
|
if bytes.Equal(raw, held[module]) {
|
|
continue
|
|
}
|
|
if err := c.Store.Put(DeclaredState, module, k); err != nil {
|
|
problems = append(problems, fmt.Errorf("%s: %w", module, err))
|
|
continue
|
|
}
|
|
written = append(written, module)
|
|
}
|
|
for module := range held {
|
|
if !placed[module] {
|
|
if err := c.Store.Delete(DeclaredState, module); err != nil {
|
|
problems = append(problems, fmt.Errorf("%s: %w", module, err))
|
|
}
|
|
}
|
|
}
|
|
return written, errors.Join(problems...)
|
|
}
|
|
|
|
// ---- the predefined list and the pull requests from the indexer ------------------------------------------
|
|
|
|
// indexerList is the indexer's predefined list as the mirror's list holds one: the keys, and the commit.
|
|
func (c *Config) indexerList() (PredefinedList, bool, error) {
|
|
e, err := c.Store.Get(IndexerPredefinedRead, catalog.PredefinedByListKey())
|
|
if err != nil || e == nil {
|
|
return PredefinedList{}, false, err
|
|
}
|
|
var kept catalog.PredefinedByListValue
|
|
if err := json.Unmarshal(e.Value, &kept); err != nil {
|
|
return PredefinedList{}, false, fmt.Errorf("the indexer's predefined list is not one: %w", err)
|
|
}
|
|
list := PredefinedList{Keys: map[string]PredefinedKey{}, Commit: kept.Commit}
|
|
listed, err := json.Marshal(kept.List)
|
|
if err == nil {
|
|
err = json.Unmarshal(listed, &list.Keys)
|
|
}
|
|
if err != nil {
|
|
return PredefinedList{}, false, fmt.Errorf("the indexer's predefined list is not predefined.json: %w", err)
|
|
}
|
|
return list, true, nil
|
|
}
|
|
|
|
// candidates is every open pull request's config declarations as the indexer keeps them, by pull key.
|
|
func (c *Config) candidates() (map[string]PullDecl, error) {
|
|
out := map[string]PullDecl{}
|
|
err := c.each(CandidatesRead, func(key string, raw []byte) error {
|
|
var cand catalog.ConfigCandidatesByPullValue
|
|
if err := json.Unmarshal(raw, &cand); err != nil || cand.Pull == "" {
|
|
return fmt.Errorf("the indexer's config candidate %s is not one", key)
|
|
}
|
|
p := PullDecl{Pull: cand.Pull, Head: cand.Head, Declares: map[string]map[string]Declaration{}, At: timeOf(cand.At)}
|
|
for module, keys := range cand.Declares {
|
|
raw, _ := json.Marshal(keys)
|
|
var d map[string]Declaration
|
|
if err := json.Unmarshal(raw, &d); err != nil {
|
|
return fmt.Errorf("the indexer's config candidate %s declares %s unreadably", key, module)
|
|
}
|
|
for k, decl := range d {
|
|
if !decl.HasDefault() {
|
|
decl.Default = nil
|
|
}
|
|
d[k] = decl
|
|
}
|
|
p.Declares[module] = d
|
|
}
|
|
out[pullKey(cand.Pull)] = p
|
|
return nil
|
|
})
|
|
return out, err
|
|
}
|
|
|
|
// takeCandidates is the indexer's config candidates taken in place of pull-declares, after the take-over: each one
|
|
// whose head or declarations differ from what `pulls` holds is declared again, its held values judged again (ADR 0502
|
|
// §6). The caller holds the lock.
|
|
func (c *Config) takeCandidates() []string {
|
|
cands, err := c.candidates()
|
|
if err != nil {
|
|
return []string{"the indexer's config candidates cannot be read: " + err.Error()}
|
|
}
|
|
var s Sources
|
|
if err := c.readPulls(&s); err != nil {
|
|
return []string{err.Error()}
|
|
}
|
|
var said []string
|
|
for _, k := range sortedKeys(cands) {
|
|
p := cands[k]
|
|
if have, ok := s.Pulls[k]; ok && have.Head == p.Head && declaresDigest(have.Declares) == declaresDigest(p.Declares) {
|
|
continue
|
|
}
|
|
a, err := c.declarePull(p.Pull, p.Head, p.Declares)
|
|
if err != nil {
|
|
said = append(said, fmt.Sprintf("%s from the indexer: %v", p.Pull, err))
|
|
continue
|
|
}
|
|
said = append(said, a.Said+" (from the indexer)")
|
|
}
|
|
return said
|
|
}
|
|
|
|
// declaresDigest is what a pull request's declarations hold a value to, in one encoding.
|
|
func declaresDigest(d map[string]map[string]Declaration) string {
|
|
flat := map[string]string{}
|
|
for m, keys := range d {
|
|
for k, decl := range keys {
|
|
flat[m+"."+k] = declDigest(decl)
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(flat)
|
|
return string(raw)
|
|
}
|
|
|
|
// Declared is the read of `declared`: every module's key, or one whole.
|
|
func (c *Config) Declared(module string) (any, error) {
|
|
if module != "" {
|
|
e, err := c.Store.Get(DeclaredState, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("declared holds no %s: it is placed nowhere, or its manifest cannot be read", module)
|
|
}
|
|
return json.RawMessage(e.Value), nil
|
|
}
|
|
rows := map[string]any{}
|
|
err := c.each(DeclaredState, func(key string, raw []byte) error {
|
|
var k DeclaredKept
|
|
if json.Unmarshal(raw, &k) == nil {
|
|
rows[key] = map[string]any{"manifest": k.Manifest, "commit": k.Commit, "keys": len(k.Config)}
|
|
}
|
|
return nil
|
|
})
|
|
sw, _ := c.source()
|
|
return map[string]any{"declared": rows, "count": len(rows), "source": sw}, err
|
|
}
|
|
|
|
// Resync reads every source again and writes what differs: the published list, the resolved values, `declared`, and
|
|
// the indexer's config candidates (ADR 0565 §15).
|
|
func (c *Config) Resync() (any, error) {
|
|
r, err := c.Reconcile()
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
out := map[string]any{"resolution": r}
|
|
if sw, err := c.source(); err == nil && sw.Source == SourceIndexer {
|
|
out["candidates"] = c.takeCandidates()
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// timeOf is an optional time of a seat's value as a time, the zero time when it is absent.
|
|
func timeOf(t *time.Time) time.Time {
|
|
if t == nil {
|
|
return time.Time{}
|
|
}
|
|
return *t
|
|
}
|