// Package assign is the assigner's judgement, as functions over what it read from the bus: the composition check of a // change (novox/hq ADR 0446 clauses 1 to 5, ADR 0528 clause 5, issue 1297), the six rules of what each node should // run (ADR 0429 §6, ADR 0528 clause 3) and the comparison with what the controller sends (ADR 0528 clause 4). It reads // and writes nothing itself: the program hands it what it watched and writes what it answers. package assign import ( "encoding/json" "fmt" "slices" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/delivery" ) // SDKRepository is the repository of mesh-sdk: a group with a member there changes the composition itself. const SDKRepository = "novox/mesh-sdk" // NotJudged names, in a composition's warnings, the failures of ADR 0446 clause 1 the assigner cannot judge from the // bus, because what they need is only in the controller's facts snapshot, which is not on the bus yet (issue 1461; // the design limit of R4g, issue 1297): said on every verdict, never passed silently. const NotJudged = "not judged until the facts snapshot is on the bus (issue 1461): a consumer left out of a grant, a " + "credential left bound elsewhere, a stored setting the changed definition cannot keep, and a new module the " + "node-engine's validator would refuse" // Mesh is the mesh as the trunk holds it: every module's manifest by name, the manifests that cannot be read with // why, and each node's open assignments. type Mesh struct { Manifests map[string]catalogue.Manifest Unreadable map[string]string Assigned map[string][]string // Digests is every trunk module's manifest digest in the indexer's manifests bucket, readable or not: what a // composer at another mesh-sdk reads (issue 1622). Digests map[string]string // Input is the rest of the mesh as the nodes compose from it, filled by mesh-sdk's frombus from the catalogue's // placement and the nodes' records (novox/hq ADR 0547 §2, ADR 0549 §3, issue 1126), so the check and the nodes share // one composer's input; nil before any placement is read, when the nodes and their assignments alone are composed. Input *compose.Input // InputUnread says why that input could not be read from the bus, when it could not: the check then composes the // nodes and their assignments alone, and says so in its warnings. InputUnread string } // Change is a group's change as the indexer's candidate holds it: the manifest of every module it moves or adds, those // it cannot read and why, the modules it removes, every module defined twice with where, the heads of a mesh-sdk // member (empty when none), and how many modules it rebuilds. type Change struct { Group, Heads string Manifests map[string]catalogue.Manifest Unreadable map[string]string Removed []string Twice map[string][]string SDKHead string Rebuilt int // Digests is the manifest digest of every module the change moves or adds, readable by the assigner or not, and // Members the head of each member by repository: what a composer at the group's mesh-sdk reads (issues 1622, 1623). Digests map[string]string Members map[string]string // Held are the approved held values of the group's pull requests at the heads judged (mesh-config's `kept`, novox/hq ADR // 0565 §5, ADR 0502 §4): each laid over its module's values on its node in the change's composition, and named. Held []delivery.KeptValue } // HeldOf are the held values kept for the pull requests the candidate names, each at the head it names: one of another // pull request, or at another head, is never laid (ADR 0502 §4). kept is mesh-config's `kept` by key. func HeldOf(kept map[string][]byte, heads map[string]string) []delivery.KeptValue { var out []delivery.KeptValue for _, raw := range kept { var k delivery.KeptValue if json.Unmarshal(raw, &k) != nil || k.Node == "" || k.Module == "" || k.Key == "" { continue } if head, ok := heads[k.Pull]; ok && head == k.Head { out = append(out, k) } } sort.Slice(out, func(i, j int) bool { a, b := out[i], out[j] return a.Pull+a.Node+a.Module+a.Key < b.Pull+b.Node+b.Module+b.Key }) return out } // layHeld lays each held value over its module's values on its node, as a layer of its own after the rest, and answers // the words that name each; judged first against the head's declaration of its key (catalogue.ValueProblems), a value the // declaration refuses, or a key it does not declare, is a failure. func layHeld(in *compose.Input, held []delivery.KeptValue) (said, fails []string) { for _, k := range held { var v any if err := json.Unmarshal(k.Value, &v); err != nil { said = append(said, fmt.Sprintf("the held value %s of %s on %s for %s cannot be read, so it is not laid: %v", k.Key, k.Module, k.Node, k.Pull, err)) continue } d, declared := in.Catalogue[k.Module].Settings[k.Key] if !declared { fails = append(fails, fmt.Sprintf("the held value %s of %s on %s for %s: the head declares no such key", k.Key, k.Module, k.Node, k.Pull)) continue } if problems := catalogue.ValueProblems(d, v); len(problems) > 0 { fails = append(fails, fmt.Sprintf("the held value %s of %s on %s for %s does not hold to the head's declaration: %s", k.Key, k.Module, k.Node, k.Pull, strings.Join(problems, "; "))) continue } at := slices.IndexFunc(in.Nodes, func(n compose.Node) bool { return n.Name == k.Node }) if at < 0 { said = append(said, fmt.Sprintf("the held value %s of %s for %s names %s, which runs no module of the mesh: not laid", k.Key, k.Module, k.Pull, k.Node)) continue } if in.Nodes[at].Settings == nil { in.Nodes[at].Settings = map[string][]catalogue.Layer{} } in.Nodes[at].Settings[k.Module] = append(in.Nodes[at].Settings[k.Module], catalogue.Layer{From: "held for " + k.Pull, Values: map[string]any{k.Key: v}}) said = append(said, fmt.Sprintf("held value laid: %s of %s on %s, held for %s at %.12s, approved by %s (novox/hq ADR 0502 §4)", k.Key, k.Module, k.Node, k.Pull, k.Head, k.Approved)) } return said, fails } // Touched is the machines a change touches (ADR 0446 clause 3): those running a module the change moves, adds or // removes, those running a consumer of a provision such a module provides (before or after), and those running a // provider of a provision such a module requires after the change. A module the change adds that is assigned nowhere // touches no machine. A change of placement is no change of a repository (ADR 0567 §4): the seat // mesh-placement judges it. func Touched(m Mesh, c Change) []string { moved := map[string]bool{} for name := range c.Manifests { moved[name] = true } for name := range c.Unreadable { moved[name] = true } for _, name := range c.Removed { moved[name] = true } provided, required := map[string]bool{}, map[string]bool{} for name := range moved { for _, man := range []catalogue.Manifest{m.Manifests[name], c.Manifests[name]} { for _, o := range man.Provides { provided[o.Name] = true } } for _, r := range c.Manifests[name].Requires { required[r] = true } } var out []string for node, modules := range m.Assigned { for _, mod := range modules { man := m.Manifests[mod] if moved[mod] || slices.ContainsFunc(man.Requires, func(r string) bool { return provided[r] }) || slices.ContainsFunc(man.Provides, func(o catalogue.Offer) bool { return required[o.Name] }) { out = append(out, node) break } } } sort.Strings(out) return out } // input is the composition's input for the mesh's catalogue and assignments: the nodes by name with what is assigned // to them, and every seat the assigned modules claim as held there. What the bus does not carry (places on the // private network, settings, credentials, licences) is left empty, so a trunk composition and a change's are made // with the same gaps and compared (ADR 0446 clause 1: what was already broken is said and fails nothing). func input(catalog map[string]catalogue.Manifest, assigned map[string][]string) compose.Input { in := compose.Input{Catalogue: catalog} nodes := make([]string, 0, len(assigned)) for n := range assigned { nodes = append(nodes, n) } sort.Strings(nodes) for _, n := range nodes { in.Nodes = append(in.Nodes, compose.Node{Name: n, Assigned: slices.Clone(assigned[n])}) for _, mod := range assigned[n] { for _, cl := range catalog[mod].Claims { scope := cl.Scope if seat, ok := catalogue.SeatNamed(cl.Name); ok && scope == "" { scope = seat.Scope } in.Holdings = append(in.Holdings, catalogue.Held{Claim: cl.Name, Scope: scope, Node: n, Module: mod}) } } } return in } // Compose is the composition check of change c over the mesh m, composed with the mesh-sdk version sdk: every touched // machine resolved as the trunk holds it and as the change leaves it. It fails on a manifest of the change that cannot // be read, a module a machine runs that the change removes, a module defined twice, and a machine that resolves on the // trunk and not with the change; a machine that does not resolve on either is said and fails nothing. It warns past // CompositionRebuildWarning modules rebuilt, says what it does not judge (NotJudged), and for a group with a mesh-sdk // member says it composed with its own mesh-sdk, not the member's head (issue 1621). took is how long it took. func Compose(m Mesh, c Change, sdk string, took time.Duration) delivery.CompositionVerdict { v := delivery.CompositionVerdict{Group: c.Group, Heads: c.Heads, SDK: sdk, Took: took.Milliseconds()} var fails []string failAt := func(machine, words string) { if v.Machine == "" && machine != "" { v.Machine = machine } fails = append(fails, words) } for _, name := range sortedKeys(c.Unreadable) { failAt("", fmt.Sprintf("the manifest of %s cannot be read: %s", name, c.Unreadable[name])) } for _, name := range sortedKeys(c.Twice) { failAt("", fmt.Sprintf("%s is defined twice, in %s", name, strings.Join(c.Twice[name], " and "))) } machines := Touched(m, c) v.Machines = machines for _, node := range machines { for _, name := range c.Removed { if slices.Contains(m.Assigned[node], name) { failAt(node, fmt.Sprintf("%s runs %s, which the change removes from its source", node, name)) } } } trunk := input(m.Manifests, m.Assigned) changed := map[string]catalogue.Manifest{} for name, man := range m.Manifests { changed[name] = man } for name, man := range c.Manifests { changed[name] = man } for _, name := range c.Removed { delete(changed, name) } after := input(changed, m.Assigned) laid, heldFails := layHeld(&after, c.Held) v.Warnings = append(v.Warnings, laid...) for _, f := range heldFails { failAt("", f) } for _, node := range machines { _, before := compose.Resolve(trunk, node) _, now := compose.Resolve(after, node) switch { case now == nil: case before != nil: v.Warnings = append(v.Warnings, fmt.Sprintf("%s does not compose on the trunk either, so the change is not blamed: %v", node, before)) default: failAt(node, fmt.Sprintf("%s composes on the trunk and not with the change: %v", node, now)) } } if c.SDKHead != "" { v.Warnings = append(v.Warnings, fmt.Sprintf("composed with the assigner's own mesh-sdk %s, not the group's mesh-sdk head %s, "+ "and a field the running controller cannot read is not judged (issues 1621, 1622, 1623)", sdk, c.SDKHead)) } return finish(v, m, c, machines, fails) } // finish adds what every composition says beside its own judgement (more modules rebuilt than CompositionRebuildWarning, // what is not judged, the machines not composed, a change that touches none) and its state from fails. func finish(v delivery.CompositionVerdict, m Mesh, c Change, machines []string, fails []string) delivery.CompositionVerdict { if c.Rebuilt > delivery.CompositionRebuildWarning { v.Warnings = append(v.Warnings, fmt.Sprintf("the merge rebuilds %d modules, more than %d", c.Rebuilt, delivery.CompositionRebuildWarning)) } v.Warnings = append(v.Warnings, NotJudged) if could := CouldRun(m, c, machines); len(could) > 0 { v.Warnings = append(v.Warnings, "not judged until the facts snapshot is on the bus (issue 1461, ADR 0537 clause 1): "+ "the machines that could run a moved or added module and are not assigned it: "+strings.Join(could, ", ")) } if grants := GrantsAlone(m, c, machines); len(grants) > 0 { v.Warnings = append(v.Warnings, "not judged until the facts snapshot is on the bus (issue 1461, ADR 0537 clause 1): "+ "the machines whose grants alone the change alters: "+strings.Join(grants, ", ")) } if len(machines) == 0 { v.Warnings = append(v.Warnings, "the change touches no machine: no module it moves is assigned anywhere") } if len(fails) > 0 { v.State, v.Words = delivery.CompositionFail, strings.Join(fails, "; ") return v } v.State = delivery.CompositionPass return v } // CouldRun is, by name, every machine of the mesh not composed that is not assigned a module the change moves or adds // (ADR 0537 clause 1): which of them could run it, the bus does not say (capabilities, settings and places are only in // the facts snapshot), so each is named as not judged. func CouldRun(m Mesh, c Change, composed []string) []string { var out []string for node, modules := range m.Assigned { if slices.Contains(composed, node) { continue } for name := range c.Manifests { if !slices.Contains(modules, name) { out = append(out, node) break } } } sort.Strings(out) return out } // grantsOf is what of a manifest makes grants: its bus declarations and the provisions it requires and offers. func grantsOf(man catalogue.Manifest) string { var offers []string for _, o := range man.Provides { offers = append(offers, o.Name) } return fmt.Sprint(man.Requires, offers, man.Reads, man.State, man.Emits, man.Consumes, man.Invokes, man.Claims, man.Grants) } // GrantsAlone is, by name, every machine not composed whose grants alone the change alters (ADR 0537 clause 1): for a // moved module whose grants differ from the trunk's, the machines assigned the bus's holder (whose user list carries // every module's bus grants) and those assigned a provider of a provision it requires, before or after. func GrantsAlone(m Mesh, c Change, composed []string) []string { provisions := map[string]bool{} altered := false for name, man := range c.Manifests { before, had := m.Manifests[name] if had && grantsOf(before) == grantsOf(man) { continue } altered = true for _, r := range append(slices.Clone(man.Requires), before.Requires...) { provisions[r] = true } } if !altered { return nil } var out []string for node, modules := range m.Assigned { if slices.Contains(composed, node) { continue } for _, mod := range modules { man := m.Manifests[mod] if man.ClaimsSeat("mesh-broker") || slices.ContainsFunc(man.Provides, func(o catalogue.Offer) bool { return provisions[o.Name] }) { out = append(out, node) break } } } sort.Strings(out) return out } // withOverlay is base with every manifest of over laid over it, a copy. func withOverlay(base, over map[string]catalogue.Manifest) map[string]catalogue.Manifest { out := make(map[string]catalogue.Manifest, len(base)+len(over)) for name, man := range base { out[name] = man } for name, man := range over { out[name] = man } return out } func sortedKeys[V any](m map[string]V) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out }