The seat mesh-catalog's four verbs answered from the indexer's modules, manifests and graph, with the old catalog's field names (captured answers as fixtures); served once the indexer claims the seat, in the release after L3's (ADR 0604 §1, §2)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// CatalogTools are the seat mesh-catalog's four verbs as tools named <seat>.<verb>, with the descriptions and inputs the
|
||||
// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the
|
||||
// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not.
|
||||
func CatalogTools(c index.CatalogRead) []stdio.Tool {
|
||||
seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
|
||||
runs := map[string]func(map[string]any) (any, error){
|
||||
"modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() },
|
||||
"module": func(a map[string]any) (any, error) { return c.CatalogModuleAnswer(str(a, "module")) },
|
||||
"provides": func(a map[string]any) (any, error) { return c.CatalogProvidesAnswer(str(a, "provision")) },
|
||||
"dependents": func(a map[string]any) (any, error) { return c.CatalogDependentsAnswer(str(a, "module")) },
|
||||
}
|
||||
var out []stdio.Tool
|
||||
for _, v := range seat.Serves {
|
||||
if run, ok := runs[v.Name]; ok {
|
||||
out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs.
|
||||
func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead {
|
||||
return index.CatalogRead{
|
||||
Modules: func() (map[string]delivery.Module, error) {
|
||||
keys, err := modules.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]delivery.Module{}
|
||||
for _, k := range keys {
|
||||
e, err := modules.Get(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("modules.%s: %w", k, err)
|
||||
}
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
if m, err := delivery.DecodeModule(e.Value); err == nil {
|
||||
out[k] = m
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
Manifest: func(digest string) (string, error) {
|
||||
key, err := delivery.ManifestsKey(digest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
e, err := manifests.Get(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if e == nil {
|
||||
return "", fmt.Errorf("the manifests state holds no %s", digest)
|
||||
}
|
||||
kept, err := delivery.DecodeManifest(e.Value)
|
||||
return kept.JSON, err
|
||||
},
|
||||
Graph: func() (delivery.Graph, error) {
|
||||
keys, err := graph.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := delivery.Graph{}
|
||||
for _, k := range keys {
|
||||
e, err := graph.Get(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("graph.%s: %w", k, err)
|
||||
}
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
if n, err := delivery.DecodeGraphNode(e.Value); err == nil {
|
||||
g[k] = n
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// catalogTools are the seat's verbs over the indexer's own states, as main serves them once the indexer claims the seat.
|
||||
func catalogTools() []stdio.Tool {
|
||||
return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(delivery.GraphState)))
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// The seat mesh-catalog's verbs, answered from the indexer's own states (novox/hq ADR 0604 §1, §2): modules, module,
|
||||
// provides and dependents read the modules state, the manifests state and the module graph, and write nothing. The
|
||||
// indexer serves no request subject (ADR 0437 §4): its tools bundle serves them as <seat>.<verb>. Each answer keeps the
|
||||
// field names of the old catalog module's tool it replaces (catalog_modules, catalog_module, catalog_provides,
|
||||
// catalog_dependents), so a caller that moves to the seat reads the same fields.
|
||||
|
||||
// CatalogRead reads the states the seat's verbs answer from.
|
||||
type CatalogRead struct {
|
||||
// Modules is every value of the modules state, by module.
|
||||
Modules func() (map[string]delivery.Module, error)
|
||||
// Manifest is the manifest kept under a digest in the manifests state, as its JSON.
|
||||
Manifest func(digest string) (string, error)
|
||||
// Graph is the module graph, by module.
|
||||
Graph func() (delivery.Graph, error)
|
||||
}
|
||||
|
||||
// CatalogEntry is one module of the catalog: its repository, its directory there (delivery.RootPath for the root), the
|
||||
// merge commit it was last changed at, and its fingerprint or why it has none.
|
||||
type CatalogEntry struct {
|
||||
Module string `json:"module"`
|
||||
Repository string `json:"repository"`
|
||||
Path string `json:"path"`
|
||||
Commit string `json:"commit"`
|
||||
FP string `json:"fp,omitempty"`
|
||||
None string `json:"none,omitempty"`
|
||||
}
|
||||
|
||||
// CatalogModule is one module whole: its entry and its manifest as the trunk holds it.
|
||||
type CatalogModule struct {
|
||||
CatalogEntry
|
||||
Manifest json.RawMessage `json:"manifest"`
|
||||
}
|
||||
|
||||
// CatalogEnd is a module that provides, requires or depends on something, with its commit; Scope is a provider's scope.
|
||||
type CatalogEnd struct {
|
||||
Module string `json:"module"`
|
||||
Commit string `json:"commit"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
func entryOf(name string, m delivery.Module) CatalogEntry {
|
||||
return CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2}
|
||||
}
|
||||
|
||||
// CatalogModulesAnswer is the verb modules: every module the modules state holds, by name.
|
||||
func (c CatalogRead) CatalogModulesAnswer() (map[string]any, error) {
|
||||
mods, err := c.Modules()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
|
||||
}
|
||||
out := []CatalogEntry{}
|
||||
for _, name := range slices.Sorted(maps.Keys(mods)) {
|
||||
out = append(out, entryOf(name, mods[name]))
|
||||
}
|
||||
return map[string]any{"modules": out}, nil
|
||||
}
|
||||
|
||||
// CatalogModuleAnswer is the verb module: one module whole, its manifest included.
|
||||
func (c CatalogRead) CatalogModuleAnswer(name string) (map[string]any, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("name the module")
|
||||
}
|
||||
mods, err := c.Modules()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
|
||||
}
|
||||
m, ok := mods[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the trunk holds no module %s", name)
|
||||
}
|
||||
one := CatalogModule{CatalogEntry: entryOf(name, m)}
|
||||
if m.Manifest == "" {
|
||||
return nil, fmt.Errorf("the indexer keeps no manifest of %s", name)
|
||||
}
|
||||
raw, err := c.Manifest(m.Manifest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err)
|
||||
}
|
||||
one.Manifest = json.RawMessage(raw)
|
||||
return map[string]any{"module": one}, nil
|
||||
}
|
||||
|
||||
// CatalogProvidesAnswer is the verb provides: the modules that provide a provision, each with its scope (a module provides
|
||||
// its own name, at node scope), and the modules that require it, from the module graph's provision edges.
|
||||
func (c CatalogRead) CatalogProvidesAnswer(provision string) (map[string]any, error) {
|
||||
provision = strings.TrimSpace(provision)
|
||||
if provision == "" {
|
||||
return nil, fmt.Errorf("name the provision")
|
||||
}
|
||||
g, mods, err := c.graphAndModules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provides, requires := []CatalogEnd{}, []CatalogEnd{}
|
||||
for _, name := range g.Providers(provision) {
|
||||
end := CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode}
|
||||
if name != provision {
|
||||
end.Scope = c.scopeOf(mods[name], provision)
|
||||
}
|
||||
provides = append(provides, end)
|
||||
}
|
||||
for _, name := range g.Requirers(provision) {
|
||||
requires = append(requires, CatalogEnd{Module: name, Commit: mods[name].Commit})
|
||||
}
|
||||
return map[string]any{"provision": provision, "provides": provides, "requires": requires}, nil
|
||||
}
|
||||
|
||||
// scopeOf is the scope the module's manifest offers provision at, "" when its manifest cannot be read.
|
||||
func (c CatalogRead) scopeOf(m delivery.Module, provision string) string {
|
||||
if m.Manifest == "" {
|
||||
return ""
|
||||
}
|
||||
raw, err := c.Manifest(m.Manifest)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
man, err := catalogue.Decode([]byte(raw))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, o := range man.Provides {
|
||||
if o.Name == provision {
|
||||
return o.At()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CatalogDependentsAnswer is the verb dependents: the modules whose build edges reach the module, directly or through
|
||||
// others, from the module graph.
|
||||
func (c CatalogRead) CatalogDependentsAnswer(module string) (map[string]any, error) {
|
||||
module = strings.TrimSpace(module)
|
||||
if module == "" {
|
||||
return nil, fmt.Errorf("name the module")
|
||||
}
|
||||
g, mods, err := c.graphAndModules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []CatalogEnd{}
|
||||
for _, name := range g.Dependents(module) {
|
||||
out = append(out, CatalogEnd{Module: name, Commit: mods[name].Commit})
|
||||
}
|
||||
return map[string]any{"module": module, "dependents": out}, nil
|
||||
}
|
||||
|
||||
func (c CatalogRead) graphAndModules() (delivery.Graph, map[string]delivery.Module, error) {
|
||||
g, err := c.Graph()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("the module graph cannot be read: %w", err)
|
||||
}
|
||||
mods, err := c.Modules()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
|
||||
}
|
||||
return g, mods, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// capturedKeys are the field names of the first object at path (a list's first element when the value is a list) in the
|
||||
// old catalog module's answer captured with mesh_call on 2026-11-03 (testdata/catalog; trimmed to a few modules, and the
|
||||
// manifest of catalog_module to three of its keys).
|
||||
func capturedKeys(t *testing.T, file string, path ...string) []string {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", "catalog", file))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return keysAt(t, raw, path...)
|
||||
}
|
||||
|
||||
func keysAt(t *testing.T, raw []byte, path ...string) []string {
|
||||
t.Helper()
|
||||
var v any
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range path {
|
||||
v = v.(map[string]any)[p]
|
||||
if l, ok := v.([]any); ok {
|
||||
v = l[0]
|
||||
}
|
||||
}
|
||||
var out []string
|
||||
for k := range v.(map[string]any) {
|
||||
out = append(out, k)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// catalogVerbsAnswerFromTheStates: the seat mesh-catalog's four verbs answer from the indexer's modules, manifests and
|
||||
// graph, with the field names of the old catalog module's answers (ADR 0604 §1, §2).
|
||||
func catalogVerbsAnswerFromTheStates(t *testing.T) {
|
||||
withAcme := strings.Replace(renamed(t, "audit-logger", "acme"), `"module": "acme",`,
|
||||
`"module": "acme", "provides": [{"name": "internal-acme-ca", "scope": "mesh", "answered-by": "helper"}],`, 1)
|
||||
ix, mods := newFormIndexer(t, mod{"mesh-tools", renamed(t, "avahi", "mesh-tools")},
|
||||
mod{"avahi", standingOn(t, catalogueManifest(t, "avahi"), "mesh-tools")},
|
||||
mod{"ca-trust", catalogueManifest(t, "ca-trust")}, mod{"acme", withAcme})
|
||||
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
|
||||
c := CatalogRead{
|
||||
Modules: func() (map[string]delivery.Module, error) {
|
||||
out := map[string]delivery.Module{}
|
||||
for k := range mods.vals {
|
||||
m, _ := moduleOf(t, mods, k)
|
||||
out[k] = m
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
Manifest: func(digest string) (string, error) {
|
||||
raw, _, err := ix.manifestRaw(context.Background(), digest)
|
||||
return raw, err
|
||||
},
|
||||
Graph: func() (delivery.Graph, error) {
|
||||
g := delivery.Graph{}
|
||||
for k := range ix.Graph.(*memStore).vals {
|
||||
g[k] = graphOf(t, ix, k)
|
||||
}
|
||||
return g, nil
|
||||
},
|
||||
}
|
||||
answer := func(v map[string]any, err error) []byte {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(v)
|
||||
return raw
|
||||
}
|
||||
contains := func(got, want []string) bool {
|
||||
for _, w := range want {
|
||||
if !slices.Contains(got, w) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
raw := answer(c.CatalogModulesAnswer())
|
||||
if got, want := keysAt(t, raw, "modules"), capturedKeys(t, "catalog_modules.json", "modules"); !contains(got, want) {
|
||||
t.Errorf("modules' fields %v, want the old catalog's %v", got, want)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"module":"avahi"`) || !strings.Contains(string(raw), `"fp":"src2:`) {
|
||||
t.Errorf("modules = %s", raw)
|
||||
}
|
||||
raw = answer(c.CatalogModuleAnswer("ca-trust"))
|
||||
want := slices.DeleteFunc(capturedKeys(t, "catalog_module.json", "module"), func(k string) bool { return k == "ref" }) // the trunk's, no ref
|
||||
if got := keysAt(t, raw, "module"); !contains(got, want) || !strings.Contains(string(raw), `"internal-acme-ca"`) {
|
||||
t.Errorf("module ca-trust = %s, want the fields %v and its manifest", raw, want)
|
||||
}
|
||||
if _, err := c.CatalogModuleAnswer("gone"); err == nil || !strings.Contains(err.Error(), "no module gone") {
|
||||
t.Errorf("a module the trunk does not hold: %v", err)
|
||||
}
|
||||
raw = answer(c.CatalogProvidesAnswer("internal-acme-ca"))
|
||||
if got, want := keysAt(t, raw, "provides"), capturedKeys(t, "catalog_provides.json", "provides"); !reflect.DeepEqual(got, want) ||
|
||||
!reflect.DeepEqual(keysAt(t, raw, "requires"), capturedKeys(t, "catalog_provides.json", "requires")) ||
|
||||
!strings.Contains(string(raw), `"provides":[{"module":"acme","commit":"`+merge+`","scope":"mesh"}]`) ||
|
||||
!strings.Contains(string(raw), `"requires":[{"module":"ca-trust"`) {
|
||||
t.Errorf("provides internal-acme-ca = %s", raw)
|
||||
}
|
||||
raw = answer(c.CatalogDependentsAnswer("mesh-tools"))
|
||||
if got, want := keysAt(t, raw), capturedKeys(t, "catalog_dependents.json"); !reflect.DeepEqual(got, want) ||
|
||||
!strings.Contains(string(raw), `"dependents":[{"module":"avahi"`) {
|
||||
t.Errorf("dependents of mesh-tools = %s", raw)
|
||||
}
|
||||
raw = answer(c.CatalogDependentsAnswer("mesh-build-go")) // the build module reaches every Go module it builds
|
||||
if !strings.Contains(string(raw), `"avahi"`) || !strings.Contains(string(raw), `"mesh-tools"`) {
|
||||
t.Errorf("dependents of mesh-build-go = %s", raw)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ func withManifests(f delivery.Forge, manifests map[string]string, locks map[stri
|
||||
func TestAMergedManifestIsKeptOnceByItsDigestAndNamedByTheModule(t *testing.T) {
|
||||
t.Run("kept by its digest and named by the module", mergedManifestIsKeptByItsDigestAndNamedByTheModule)
|
||||
t.Run("the same manifest applied again is written once", sameManifestAppliedAgainIsWrittenOnce)
|
||||
t.Run("the seat mesh-catalog's verbs answer from the states", catalogVerbsAnswerFromTheStates)
|
||||
}
|
||||
|
||||
func mergedManifestIsKeptByItsDigestAndNamedByTheModule(t *testing.T) { // 1449/AC1
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"dependents": [],
|
||||
"module": "mesh-build-go"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"module": {
|
||||
"commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026",
|
||||
"manifest": {
|
||||
"module": "gitea",
|
||||
"requires": [
|
||||
"postgres-database",
|
||||
"route",
|
||||
"secret"
|
||||
],
|
||||
"version": "1"
|
||||
},
|
||||
"module": "gitea",
|
||||
"path": "modules/gitea",
|
||||
"ref": "refs/pull/571/head",
|
||||
"repository": "novox/mesh-catalog"
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026",
|
||||
"module": "gitea",
|
||||
"path": "modules/gitea",
|
||||
"repository": "novox/mesh-catalog"
|
||||
},
|
||||
{
|
||||
"commit": "dc90b3f5bd1386747ac70f2792169babf8eeed73",
|
||||
"module": "mesh-controller",
|
||||
"path": ".",
|
||||
"repository": "novox/mesh-controller"
|
||||
},
|
||||
{
|
||||
"commit": "432326264f6c34ceec1fb67f5ca9d450b4caaa1d",
|
||||
"module": "postgres",
|
||||
"path": "modules/postgres",
|
||||
"repository": "novox/mesh-catalog"
|
||||
}
|
||||
]
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"provides": [
|
||||
{
|
||||
"commit": "432326264f6c34ceec1fb67f5ca9d450b4caaa1d",
|
||||
"module": "postgres",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"requires": [
|
||||
{
|
||||
"commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026",
|
||||
"module": "gitea"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user