Merge pull request 'Read the indexer's modules keys as stored from the view's tools, so the new-form fingerprint, path and toolchains can be checked live (issue 1562)' (#13) from fix/1562-indexer-reads into main
This commit was merged in pull request #13.
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
// Command mesh-delivery-view-tools serves the read tools of the indexer's modules (novox/hq issue 1562): the runtime launches it for a
|
||||||
|
// call, it reads the indexer's modules bucket through the view's reads (mesh-delivery-indexer.modules) and writes nothing.
|
||||||
|
// The view is the other program: the indexer serves no tool (ADR 0437 §4).
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||||
|
"mesh-delivery-view/internal/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tools are the read tools over a modules state.
|
||||||
|
func Tools(r view.Reader) []stdio.Tool {
|
||||||
|
return []stdio.Tool{
|
||||||
|
{Name: view.ModuleReadTool,
|
||||||
|
Description: "What the indexer stored for one module under modules.<module>, as stored: its fingerprint (fp, none), the " +
|
||||||
|
"new-form fingerprint (fp2) or why it has none (none2), the inputs it was computed from (inputs2), the module's " +
|
||||||
|
"directory (path) and the toolchains it was computed with. Read-only.",
|
||||||
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
||||||
|
"module": map[string]any{"type": "string", "description": "the module's name"}}},
|
||||||
|
Run: func(args map[string]any) (any, error) {
|
||||||
|
name, _ := args["module"].(string)
|
||||||
|
return r.Module(name)
|
||||||
|
}},
|
||||||
|
{Name: view.ModulesReadTool,
|
||||||
|
Description: "Every module the indexer holds, each as stored under modules.<module>. With none2 true, only the modules " +
|
||||||
|
"that have no new-form fingerprint, each with the reason (none2). Read-only.",
|
||||||
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||||
|
"none2": map[string]any{"type": "boolean", "description": "only the modules without a new-form fingerprint"}}},
|
||||||
|
Run: func(args map[string]any) (any, error) {
|
||||||
|
only, _ := args["none2"].(bool)
|
||||||
|
mods, err := r.Modules(only)
|
||||||
|
return map[string]any{"modules": mods}, err
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
state := stdio.State("mesh-delivery-indexer.modules")
|
||||||
|
r := view.Reader{State: view.StateRead{Keys: state.Keys, Get: func(key string) (json.RawMessage, error) {
|
||||||
|
e, err := state.Get(key)
|
||||||
|
if err != nil || e == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.Value, nil
|
||||||
|
}}}
|
||||||
|
if err := stdio.Serve("", Tools(r)); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"mesh-delivery-view/internal/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fake map[string]string
|
||||||
|
|
||||||
|
func (f fake) reader() view.Reader {
|
||||||
|
return view.Reader{State: view.StateRead{
|
||||||
|
Keys: func() ([]string, error) {
|
||||||
|
var k []string
|
||||||
|
for key := range f {
|
||||||
|
k = append(k, key)
|
||||||
|
}
|
||||||
|
return k, nil
|
||||||
|
},
|
||||||
|
Get: func(key string) (json.RawMessage, error) {
|
||||||
|
if v, ok := f[key]; ok {
|
||||||
|
return json.RawMessage(v), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(t *testing.T, r view.Reader, name string, args map[string]any) (any, error) {
|
||||||
|
t.Helper()
|
||||||
|
for _, tl := range Tools(r) {
|
||||||
|
if tl.Name == name {
|
||||||
|
return tl.Run(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("no tool %s", name)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
withFP2 = `{"v":3,"fp":"a1","fp2":"b2","manifest":"m1","inputs2":{"tree":"t1","bases":["x"]},"repository":"novox/r","commit":"c1","path":".","toolchains":{"go":{"version":"go1.27.1","recipe":"2"}}}`
|
||||||
|
withNone = `{"v":3,"none":"no successful trunk build recorded its inputs","none2":"builder's fingerprint not yet known","repository":"novox/r","commit":"c2","path":"modules/b"}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1562/AC1: indexer_module answers the key's value as stored: FP, FP2, None2, Inputs2, Path and Toolchains are all there.
|
||||||
|
func TestModuleToolAnswersTheStoredValue(t *testing.T) {
|
||||||
|
r := fake{"a": withFP2, "b": withNone}.reader()
|
||||||
|
for key, want := range map[string]string{"a": withFP2, "b": withNone} {
|
||||||
|
out, err := run(t, r, view.ModuleReadTool, map[string]any{"module": key})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := string(out.(json.RawMessage)); got != want {
|
||||||
|
t.Errorf("%s = %s, want %s", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ := run(t, r, view.ModuleReadTool, map[string]any{"module": "a"})
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal(out.(json.RawMessage), &m); err != nil || m["fp"] != "a1" || m["fp2"] != "b2" || m["path"] != "." || m["inputs2"] == nil || m["toolchains"] == nil {
|
||||||
|
t.Errorf("stored fields are not all there: %v %v", m, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A module with no key, and a name that is no key, are said so.
|
||||||
|
func TestModuleToolRefusesWhatIsNotThere(t *testing.T) {
|
||||||
|
r := fake{"a": withFP2}.reader()
|
||||||
|
if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": "gone"}); err == nil || !strings.Contains(err.Error(), "no module gone") {
|
||||||
|
t.Errorf("a missing module: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": ""}); err == nil {
|
||||||
|
t.Errorf("an empty name was answered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1562/AC1: indexer_modules answers every key as stored, and with none2 only those that say why they have no FP2.
|
||||||
|
func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) {
|
||||||
|
r := fake{"a": withFP2, "b": withNone}.reader()
|
||||||
|
out, err := run(t, r, view.ModulesReadTool, map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
all := out.(map[string]any)["modules"].(map[string]json.RawMessage)
|
||||||
|
if len(all) != 2 || string(all["a"]) != withFP2 || string(all["b"]) != withNone {
|
||||||
|
t.Errorf("modules = %v", all)
|
||||||
|
}
|
||||||
|
out, err = run(t, r, view.ModulesReadTool, map[string]any{"none2": true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
only := out.(map[string]any)["modules"].(map[string]json.RawMessage)
|
||||||
|
if len(only) != 1 || string(only["b"]) != withNone {
|
||||||
|
t.Errorf("none2 modules = %v", only)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
// The read tools of the indexer's modules (novox/hq issue 1562): what the indexer stored under modules.<m>, answered as
|
||||||
|
// stored. The new-form fingerprint (FP2, None2, Inputs2), the reason it has none, and the path and toolchains it was computed
|
||||||
|
// with are only there. The view reads the bucket through its reads; the reads write nothing.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The read tools, named as the module is: indexer_ and the verb.
|
||||||
|
const (
|
||||||
|
ModuleReadTool = "indexer_module"
|
||||||
|
ModulesReadTool = "indexer_modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StateRead reads the indexer's modules state, as the view's reads grant it: its keys and a key's stored value (nil for a key with none).
|
||||||
|
type StateRead struct {
|
||||||
|
Keys func() ([]string, error)
|
||||||
|
Get func(key string) (json.RawMessage, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader answers the read tools from a modules state.
|
||||||
|
type Reader struct{ State StateRead }
|
||||||
|
|
||||||
|
// Module is the stored value of modules.<module>, exactly as the indexer wrote it.
|
||||||
|
func (r Reader) Module(module string) (json.RawMessage, error) {
|
||||||
|
module = strings.TrimSpace(module)
|
||||||
|
key, err := delivery.ModulesKey(module)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.State.Get == nil {
|
||||||
|
return nil, fmt.Errorf("the modules state cannot be read here")
|
||||||
|
}
|
||||||
|
raw, err := r.State.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil, fmt.Errorf("no module %s in the modules state", module)
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modules is every key of the modules state with its stored value; with none2 only the modules that have no new-form
|
||||||
|
// fingerprint and say why (a None2 reason), which is what the fingerprint's check reads live.
|
||||||
|
func (r Reader) Modules(none2 bool) (map[string]json.RawMessage, error) {
|
||||||
|
if r.State.Keys == nil || r.State.Get == nil {
|
||||||
|
return nil, fmt.Errorf("the modules state cannot be read here")
|
||||||
|
}
|
||||||
|
keys, err := r.State.Keys()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
out := map[string]json.RawMessage{}
|
||||||
|
for _, k := range keys {
|
||||||
|
raw, err := r.State.Get(k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(raw) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if none2 {
|
||||||
|
var m struct {
|
||||||
|
None2 string `json:"none2"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &m) != nil || m.None2 == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[k] = raw
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -19,12 +19,13 @@ import (
|
|||||||
// takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.**
|
// takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.**
|
||||||
|
|
||||||
type artifact struct {
|
type artifact struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
System string `json:"system"`
|
System string `json:"system"`
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
Binary string `json:"binary"`
|
Binary string `json:"binary"`
|
||||||
|
Loads []string `json:"loads"`
|
||||||
Context *struct {
|
Context *struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
} `json:"context"`
|
} `json:"context"`
|
||||||
@@ -41,6 +42,7 @@ type manifest struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Reads []string `json:"reads"`
|
Reads []string `json:"reads"`
|
||||||
|
Tools []string `json:"tools"`
|
||||||
OwnSecrets map[string]string `json:"own-secrets"`
|
OwnSecrets map[string]string `json:"own-secrets"`
|
||||||
SecretsOwner string `json:"secrets-owner"`
|
SecretsOwner string `json:"secrets-owner"`
|
||||||
Resources []map[string]any `json:"resources"`
|
Resources []map[string]any `json:"resources"`
|
||||||
@@ -91,11 +93,23 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
|||||||
if m.SecretsOwner != "mesh-delivery-view" {
|
if m.SecretsOwner != "mesh-delivery-view" {
|
||||||
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
||||||
}
|
}
|
||||||
|
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
|
||||||
|
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
|
||||||
|
if want := []string{"indexer_module", "indexer_modules"}; !reflect.DeepEqual(m.Tools, want) {
|
||||||
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||||
|
}
|
||||||
|
for _, a := range m.Build.Artifacts {
|
||||||
|
// A bundle compiled to a binary is one executable: what the runtime loads is that binary's name, or the controller's
|
||||||
|
// module check refuses the manifest (mesh-sdk catalogue build.go, ADR 0193).
|
||||||
|
if a.Name == "tools" && !reflect.DeepEqual(a.Loads, []string{a.Binary}) {
|
||||||
|
t.Errorf("the tools artifact loads %v, want its binary %q", a.Loads, a.Binary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
|
func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
|
||||||
m, _ := load(t)
|
m, _ := load(t)
|
||||||
if len(m.Build.Artifacts) != 3 {
|
if len(m.Build.Artifacts) != 4 {
|
||||||
t.Fatalf("artifacts = %+v", m.Build.Artifacts)
|
t.Fatalf("artifacts = %+v", m.Build.Artifacts)
|
||||||
}
|
}
|
||||||
for _, a := range m.Build.Artifacts {
|
for _, a := range m.Build.Artifacts {
|
||||||
@@ -119,7 +133,7 @@ func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
|
|||||||
for _, a := range m.Build.Artifacts {
|
for _, a := range m.Build.Artifacts {
|
||||||
names[a.Name] = a.Binary
|
names[a.Name] = a.Binary
|
||||||
}
|
}
|
||||||
if names["program"] != "mesh-delivery-view" || names["cli"] != "mesh-delivery-cli" {
|
if names["program"] != "mesh-delivery-view" || names["cli"] != "mesh-delivery-cli" || names["tools"] != "mesh-delivery-view-tools" {
|
||||||
t.Errorf("artifacts = %v", names)
|
t.Errorf("artifacts = %v", names)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
"version": "1",
|
"version": "1",
|
||||||
"moved": true,
|
"moved": true,
|
||||||
"slug": "view",
|
"slug": "view",
|
||||||
|
"tools": [
|
||||||
|
"indexer_module",
|
||||||
|
"indexer_modules"
|
||||||
|
],
|
||||||
"reads": [
|
"reads": [
|
||||||
"mesh-controller.assignments",
|
"mesh-controller.assignments",
|
||||||
"mesh-controller.builds",
|
"mesh-controller.builds",
|
||||||
@@ -97,6 +101,17 @@
|
|||||||
"from": "cmd/mesh-delivery-cli",
|
"from": "cmd/mesh-delivery-cli",
|
||||||
"binary": "mesh-delivery-cli"
|
"binary": "mesh-delivery-cli"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "tools",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-delivery-view-tools",
|
||||||
|
"binary": "mesh-delivery-view-tools",
|
||||||
|
"loads": [
|
||||||
|
"mesh-delivery-view-tools"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "files",
|
"name": "files",
|
||||||
"kind": "archive",
|
"kind": "archive",
|
||||||
|
|||||||
Reference in New Issue
Block a user