The delivery repository's contracts and SDK live under modules/ as mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths renamed to match the folders (the TypeScript names already did), and every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools and mesh-sdk follows in the same group: a design as it should be, one path per module, checked by the group's check building every importer. Builds: design 47 G §427, §88 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
207 lines
7.9 KiB
Go
207 lines
7.9 KiB
Go
// Package assigngraph is the assigner's assignment graph (design 47, *Two graphs, one source of truth*; ADR 0656): the
|
|
// module versions placed on nodes, with the edges between versions, computed from the catalog graph of the branch each
|
|
// placement names, the placements and the nodes' capability readings, and kept per revision as objects of the seat
|
|
// mesh-wants' kept data assignment-revisions.
|
|
package assigngraph
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
|
|
"mesh-delivery-assigner/internal/assignment"
|
|
)
|
|
|
|
// The kinds of an edge of the assignment graph (ADR 0656; the seat's Edge).
|
|
const (
|
|
EdgeProvisioning = "provisioning"
|
|
EdgeActing = "acting"
|
|
)
|
|
|
|
// Inputs are what a revision is computed from, and nothing else (design 47 lines 1353-1356).
|
|
type Inputs struct {
|
|
// Halves is the catalog graph revision held of each watched branch, by branch.
|
|
Halves map[string]meshcatalogclient.Half
|
|
// Placements is every node's placement key, by node.
|
|
Placements map[string]mma.Assignments
|
|
// FirstBranch is the first watched branch of a module's registration, the branch a placement naming none runs
|
|
// (design 47 lines 522-523); "" when no registration is known for it.
|
|
FirstBranch func(module string) string
|
|
// Capabilities is each node's capability reading, by its digest.
|
|
Capabilities []meshwants.AssignmentRevisionCapabilitiesItem
|
|
}
|
|
|
|
// Computed is one assignment graph revision as computed, before it is numbered and kept.
|
|
type Computed struct {
|
|
Revision meshwants.AssignmentRevision
|
|
// Entries is every entry the revision lists, by its digest.
|
|
Entries map[string]meshwants.AssignmentEntry
|
|
// Uncomputed is each placed module whose entry could not be computed, as "<node>/<module>: why", sorted: it is said,
|
|
// never left out in silence.
|
|
Uncomputed []string
|
|
}
|
|
|
|
// placedAt is one placed module with what its entry is computed from.
|
|
type placedAt struct {
|
|
node string
|
|
placed mma.PlacedModule
|
|
branch string
|
|
entry meshwants.AssignmentEntry
|
|
langs map[string]bool
|
|
acts []actingRule
|
|
}
|
|
|
|
type actingRule struct{ seat, step, on, bound string }
|
|
|
|
// Compute is the assignment graph revision of in: one entry per module placed on a node whose branch's catalog graph
|
|
// holds it, its version the build the placement names or else the catalog entry's fingerprint (design 47 lines 909-914),
|
|
// with the edges of provisioning to the providers its placement binds or its node pins, and of acting from each acting
|
|
// part's version to each version its rule names (design 47 *An acting part's new version acts first*: "the assigner
|
|
// gives an edge from the acting part's version to each version its rule names").
|
|
func Compute(in Inputs) Computed {
|
|
out := Computed{Entries: map[string]meshwants.AssignmentEntry{}}
|
|
var placed []*placedAt
|
|
branches := map[string]bool{}
|
|
for _, node := range slices.Sorted(maps.Keys(in.Placements)) {
|
|
mods := slices.Clone(in.Placements[node].Modules)
|
|
slices.SortFunc(mods, func(a, b mma.PlacedModule) int { return strings.Compare(a.Module, b.Module) })
|
|
for _, m := range mods {
|
|
branch := m.Branch
|
|
if branch == "" && in.FirstBranch != nil {
|
|
branch = in.FirstBranch(m.Module)
|
|
}
|
|
if branch == "" {
|
|
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: no watched branch: its placement names none and no registration of it is known", node, m.Module))
|
|
continue
|
|
}
|
|
half, ok := in.Halves[branch]
|
|
if !ok {
|
|
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: the catalog graph of the branch %s is not held", node, m.Module, branch))
|
|
continue
|
|
}
|
|
ce, ok := half.Entries[m.Module]
|
|
if !ok {
|
|
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: the catalog graph revision %d of %s holds no entry of it", node, m.Module, half.Revision.Revision, branch))
|
|
continue
|
|
}
|
|
digest := ""
|
|
for _, ref := range half.Revision.Entries {
|
|
if ref.Module == m.Module {
|
|
digest = ref.Digest
|
|
}
|
|
}
|
|
if digest == "" {
|
|
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: the catalog graph revision %d of %s lists no digest of its entry", node, m.Module, half.Revision.Revision, branch))
|
|
continue
|
|
}
|
|
fp := ce.Fp
|
|
if m.Build != "" {
|
|
fp = m.Build
|
|
}
|
|
p := &placedAt{node: node, placed: m, branch: branch, langs: map[string]bool{},
|
|
entry: meshwants.AssignmentEntry{V: keys.Version, Node: node, Module: m.Module, Branch: branch, Fp: fp, Catalog: digest}}
|
|
for lang := range ce.Toolchains {
|
|
p.langs[lang] = true
|
|
}
|
|
for _, r := range ce.Acts {
|
|
p.acts = append(p.acts, actingRule{seat: r.Seat, step: r.Step, on: r.On, bound: r.Bound})
|
|
}
|
|
for _, r := range ce.Requires {
|
|
prov, ok := m.Bindings[r]
|
|
if !ok {
|
|
prov, ok = in.Placements[node].Pins[r]
|
|
}
|
|
if ok {
|
|
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: prov.Node, Module: prov.Module, Provision: r})
|
|
}
|
|
}
|
|
placed = append(placed, p)
|
|
branches[branch] = true
|
|
}
|
|
}
|
|
at := map[string]*placedAt{}
|
|
for _, p := range placed {
|
|
at[p.node+"/"+p.placed.Module] = p
|
|
}
|
|
for _, p := range placed {
|
|
for i, e := range p.entry.Edges {
|
|
if prov, ok := at[e.Node+"/"+e.Module]; ok {
|
|
p.entry.Edges[i].Fp = prov.entry.Fp
|
|
}
|
|
}
|
|
}
|
|
// Acting: an acting part on the entry's own node acts there (a node's seat is held on every node); else every node it
|
|
// is placed on (a seat held once for the mesh is held on one).
|
|
byModule := map[string][]*placedAt{}
|
|
for _, p := range placed {
|
|
byModule[p.placed.Module] = append(byModule[p.placed.Module], p)
|
|
}
|
|
for _, part := range slices.Sorted(maps.Keys(byModule)) {
|
|
holders := byModule[part]
|
|
for _, rule := range holders[0].acts {
|
|
for _, t := range placed {
|
|
if t.placed.Module == part || (rule.on != "" && rule.on != "every" && !t.langs[rule.on]) {
|
|
continue
|
|
}
|
|
from := holders
|
|
for _, h := range holders {
|
|
if h.node == t.node {
|
|
from = []*placedAt{h}
|
|
}
|
|
}
|
|
for _, h := range from {
|
|
t.entry.Edges = append(t.entry.Edges, meshwants.Edge{Kind: EdgeActing, Node: h.node, Module: part, Fp: h.entry.Fp,
|
|
Seat: rule.seat, Step: rule.step, Bound: rule.bound})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rev := meshwants.AssignmentRevision{V: keys.Version, Kind: meshcatalogclient.KindTrunk, Placements: assignment.ExportDigest(in.Placements),
|
|
Capabilities: in.Capabilities, Entries: []meshwants.NodeEntryRef{}}
|
|
for _, b := range slices.Sorted(maps.Keys(branches)) {
|
|
h := in.Halves[b]
|
|
rev.Catalogs = append(rev.Catalogs, meshwants.CatalogOf{Branch: b, Revision: h.Revision.Revision, Source: h.Revision.Source})
|
|
}
|
|
for _, p := range placed {
|
|
sortEdges(p.entry.Edges)
|
|
d := EntryDigest(p.entry)
|
|
out.Entries[d] = p.entry
|
|
rev.Entries = append(rev.Entries, meshwants.NodeEntryRef{Node: p.node, Module: p.placed.Module, Digest: d})
|
|
}
|
|
out.Revision = rev
|
|
slices.Sort(out.Uncomputed)
|
|
return out
|
|
}
|
|
|
|
func sortEdges(edges []meshwants.Edge) {
|
|
slices.SortFunc(edges, func(a, b meshwants.Edge) int {
|
|
return strings.Compare(edgeKey(a), edgeKey(b))
|
|
})
|
|
}
|
|
|
|
func edgeKey(e meshwants.Edge) string {
|
|
return strings.Join([]string{e.Kind, e.Node, e.Module, e.Provision, e.Seat, e.Step, e.Fp, e.Bound}, "\x00")
|
|
}
|
|
|
|
// EntryDigest is the digest an entry is stored under: the same entry, the same digest.
|
|
func EntryDigest(e meshwants.AssignmentEntry) string {
|
|
raw, _ := json.Marshal(e)
|
|
sum := sha256.Sum256(raw)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// Same says two revisions list the same entries computed from the same inputs, so the second is no new revision.
|
|
func Same(a, b meshwants.AssignmentRevision) bool {
|
|
return slices.Equal(a.Entries, b.Entries) && slices.Equal(a.Catalogs, b.Catalogs) && a.Placements == b.Placements &&
|
|
slices.Equal(a.Capabilities, b.Capabilities)
|
|
}
|