From 8b4d536a115268bd876b398db848fe6e02213d67 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 02:07:44 +0100 Subject: [PATCH] Take the indexer's verdict on a manifest instead of judging it in the controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1585, S18 (ADR 0578 §3, §6): a done key's manifest is decoded leniently and the verdict read from the indexer's state; a refused one is held and named, an unread word set aside and named, never a wait. The module check command, which judged files with no mesh and which nothing calls any more, goes with its tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- cmd/mesh-controller/check.go | 337 ------------------- cmd/mesh-controller/check_test.go | 135 -------- cmd/mesh-controller/health_check_test.go | 72 ---- cmd/mesh-controller/identity_bound_test.go | 52 --- cmd/mesh-controller/kept_helpers.go | 12 - cmd/mesh-controller/key_takein.go | 28 +- cmd/mesh-controller/key_takein_test.go | 45 ++- cmd/mesh-controller/main.go | 1 - cmd/mesh-controller/modules.go | 32 +- cmd/mesh-controller/predefined_test.go | 61 +--- cmd/mesh-controller/shares_test.go | 27 -- cmd/mesh-controller/status.go | 2 +- cmd/mesh-controller/template_test.go | 123 ------- cmd/mesh-controller/unknown_fields.go | 5 +- cmd/mesh-controller/unknown_fields_test.go | 10 +- cmd/mesh-controller/written_settings_test.go | 16 +- go.mod | 2 +- go.sum | 2 + internal/builderkeys/builderkeys.go | 25 ++ testdata/beside/CAPTURED | 2 +- 20 files changed, 102 insertions(+), 887 deletions(-) delete mode 100644 cmd/mesh-controller/check.go delete mode 100644 cmd/mesh-controller/health_check_test.go delete mode 100644 cmd/mesh-controller/identity_bound_test.go delete mode 100644 cmd/mesh-controller/template_test.go diff --git a/cmd/mesh-controller/check.go b/cmd/mesh-controller/check.go deleted file mode 100644 index b6f605f8..00000000 --- a/cmd/mesh-controller/check.go +++ /dev/null @@ -1,337 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "github.com/novox/mesh-controller/internal/link" -) - -// moduleCheck judges manifests where they are written, with no mesh (novox/hq ADR 0037, issue 148). -// -// **The same functions registration runs, and nothing the command line adds** (ADR 0035): the strict -// parse with every per-manifest problem, then the rules no single manifest can be judged against, -// over exactly the manifests given. Somebody describing their own application in their own -// repository runs this before pushing and finds out there, rather than when a running mesh refuses -// the registration or, later, when a machine applies something that resolved and should not have. -// -// **What it cannot know without a store, it says.** The mesh's own seat set is the store's (ADR -// 0122); this binary carries a compiled copy that the store overrides when loaded, so a claim on a -// mesh seat is judged fully only at registration. A seat another module declares is unknown unless -// that module's manifest is passed too. Both are printed as a note, not as a problem — a check that -// refused what it could not see would teach people to ignore it. -// -// **And every identity against every bound it meets** (novox/hq ADR 0225, issue 263): each module's -// identity, on a machine whose name is `longestMachine` characters, against the bound of every -// provision it wants that a manifest given here offers. An overflow is refused in the pull request -// that introduces it — a new requirement, a lowered bound, a longer slug — instead of on the -// provider's machine when a real machine's name first meets the module's. -// SomeManifestsVar, set by the merge gate, says the manifests given are only some of their repository's. -const SomeManifestsVar = "MESH_MODULE_CHECK_SOME" - -func moduleCheck(paths []string, out io.Writer) error { - return moduleCheckFor(paths, catalogue.DefaultLongestMachine, out) -} - -func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error { - if len(paths) == 0 { - return errors.New("module check ... — one file per module; pass every " + - "manifest of a repository together so the rules between them are checked too") - } - shelf := catalogue.Shelf{} - faulted := map[string]bool{} - failed := 0 - // The list of predefined keys at the root of the repository above each manifest (novox/hq ADR 0348 §7), read - // once per repository: a manifest naming a key is judged against the list its own tree holds. - lists := map[string]catalogue.PredefinedList{} - listOf := func(path string) (catalogue.PredefinedList, error) { - root := catalogue.RepositoryRoot(filepath.Dir(path)) - if root == "" { - return nil, nil - } - if list, read := lists[root]; read { - return list, nil - } - list, err := catalogue.ReadPredefined(root) - if err != nil { - return nil, err - } - lists[root] = list - return list, nil - } - for _, path := range paths { - raw, err := os.ReadFile(path) - if err != nil { - fmt.Fprintf(out, "%s: %v\n", path, err) - failed++ - continue - } - list, err := listOf(path) - if err != nil { - fmt.Fprintf(out, "%s: %v\n", path, err) - failed++ - continue - } - m, err := catalogue.ParseManifestWith(raw, list) - if err == nil { - // Each template's text read from the tree beside the manifest, and judged as inline content is - // (novox/hq ADR 0349 §2): a missing template is refused by name. - m, err = catalogue.WithTemplatesFrom(m, filepath.Dir(path)) - } - if err != nil { - fmt.Fprintf(out, "%s: %v\n", path, err) - failed++ - continue - } - if first, twice := shelf[m.Module]; twice { - _ = first - fmt.Fprintf(out, "%s: %s was already given; two manifests name one module\n", path, m.Module) - failed++ - continue - } - // A definition names no installation (novox/hq ADR 0112, ADR 0155): judged here, in the - // catalogue-wide test, and at registration, which refuses in the same words. - if wrong := catalogue.TrustProblems(m); len(wrong) > 0 { - for _, p := range wrong { - fmt.Fprintf(out, "%s: %s\n", path, p) - } - failed += len(wrong) - faulted[m.Module] = true - } - // A module that declares a setting names the keys of its mergeable files (novox/hq ADR 0326 §3, - // issue 574): judged where a definition is written, as at registration, never where a stored - // build is read again. - if wrong := catalogue.WrittenSettingsProblems(m); len(wrong) > 0 { - for _, p := range wrong { - fmt.Fprintf(out, "%s: %s\n", path, p) - } - failed += len(wrong) - faulted[m.Module] = true - } - // A module lives whole in one repository (novox/hq ADR 0420 §1, issue 1019): when the check is told the - // repository it runs for (MESH_CHECK_REPOSITORY, set by the build's check), an artifact building from - // another is refused, as the merge gate refuses it. A run by hand names no repository and judges none. - if repo := os.Getenv("MESH_CHECK_REPOSITORY"); repo != "" && m.Build != nil { - for _, a := range m.Build.Artifacts { - if a.Context != nil && a.Context.Repository != "" && !sameRepository(a.Context.Repository, - link.SourceMoved{Owner: ownerOf(repo), Repo: repoOf(repo)}) { - fmt.Fprintf(out, "%s: %s builds artifact %s from %s: a module lives whole in one repository "+ - "(novox/hq ADR 0420 §1)\n", path, m.Module, a.Name, a.Context.Repository) - failed++ - faulted[m.Module] = true - } - } - } - if named := catalogue.InstallationProblems(m); len(named) > 0 { - for _, p := range named { - fmt.Fprintf(out, "%s: %s\n", path, p) - } - failed += len(named) - faulted[m.Module] = true - } - shelf[m.Module] = m - } - - // Between the manifests: a seat declared twice, a use of a seat nothing declares, a claim on - // a seat that does not exist. Run only over what parsed, because a problem inside one manifest - // has already been said and would be said again here in a worse form. - // - // **Over some of a repository's manifests, a seat none of them declares is a note** (novox/hq issue 364), as - // this command's own word says above: the merge gate passes only the manifests a change touches, and says so - // with SomeManifestsVar, so a module that uses or claims a seat another module declares (the operator - // channel's, a channel bench) was refused there for a manifest it was not given. Given every manifest — the - // catalogue's own check, and registration — it stays a refusal. - some := os.Getenv(SomeManifestsVar) != "" - problems := catalogue.CatalogueProblems(shelf) - sort.Strings(problems) - for _, p := range problems { - if some && catalogue.IsUndeclaredSeat(p) { - fmt.Fprintf(out, "note: %s among the manifests given; registration judges it against the whole catalogue, "+ - "and passing the declaring module's manifest too judges it here\n", p) - continue - } - fmt.Fprintln(out, p) - failed++ - } - - // Between the manifests too: an identity against the bounds of the provisions it wants, which - // only the provider's manifest states. - identities := catalogue.IdentityProblems(shelf, longestMachine) - sort.Strings(identities) - for _, p := range identities { - fmt.Fprintln(out, p) - } - failed += len(identities) - - // And the data each module keeps (novox/hq ADR 0233): a provider that grants says what it keeps for - // its consumers, a directory a container writes is declared, and no backup line is written by hand. - data := catalogue.DataProblems(shelf) - sort.Strings(data) - for _, p := range data { - fmt.Fprintln(out, p) - } - failed += len(data) - - var names []string - for name := range shelf { - names = append(names, name) - } - sort.Strings(names) - - // **Every long-running resource says how it is ready** (novox/hq ADR 0240 rule 8): warned until the - // date, refused from it. The count is the catalogue's: its merge check keeps the number and lets a - // change lower it, never raise it. - undeclared := 0 - required := !checkNow().Before(catalogue.HealthRequiredFrom) - for _, name := range names { - missing := catalogue.Undeclared(shelf[name]) - undeclared += len(missing) - if len(missing) == 0 { - continue - } - if required { - for _, id := range missing { - fmt.Fprintf(out, "%s: %s stays up and does not say how it is ready: a long-running resource declares "+ - "health since %s (novox/hq ADR 0240 rule 8)\n", name, id, catalogue.HealthRequiredFrom.Format("2006-01-02")) - } - failed += len(missing) - faulted[name] = true - } - } - - // **A file that asks for a setting and does not say whether it is trusted counts as trusted** (novox/hq issue - // 339): listed and counted, never refused, so an author can opt out a file nothing trusts. - unsaid := 0 - for _, name := range names { - unsaid += len(catalogue.UnsaidTrust(shelf[name])) - } - - for _, name := range names { - m := shelf[name] - if faulted[name] { - continue - } - fmt.Fprintf(out, "%s: ok", name) - if n := len(m.Tools); n > 0 { - fmt.Fprintf(out, ", %d tool(s)", n) - } - if len(m.Invokes) > 0 { - fmt.Fprintf(out, ", invokes %s", joinInvokes(m.Invokes)) - } - // The state it keeps and reads (novox/hq ADR 0201), so a reviewer sees what lands on the bus. - if len(m.State) > 0 { - kept := make([]string, 0, len(m.State)) - for _, s := range m.State { - kept = append(kept, s.Name) - } - fmt.Fprintf(out, ", keeps state %s", strings.Join(kept, ", ")) - } - // And the logs it keeps, with their caps (novox/hq ADR 0297). - if len(m.Logs) > 0 { - kept := make([]string, 0, len(m.Logs)) - for _, l := range m.Logs { - kept = append(kept, fmt.Sprintf("%s (%d MiB)", l.Name, l.Cap())) - } - fmt.Fprintf(out, ", keeps log %s", strings.Join(kept, ", ")) - } - if len(m.Reads) > 0 { - fmt.Fprintf(out, ", reads %s", strings.Join(m.Reads, ", ")) - } - // The data it keeps, by class, so a reviewer sees what the mesh will protect and how. - if items := m.DataItems(); len(items) > 0 { - kept := make([]string, 0, len(items)) - for _, it := range items { - kept = append(kept, it.ID+" ("+it.Class+")") - } - fmt.Fprintf(out, ", keeps %s", strings.Join(kept, ", ")) - } - // How what it runs is ready (ADR 0240): each declared check, and what is judged by liveness alone. - var checks []string - for _, r := range m.Resources { - if h, has, _ := catalogue.ReadHealth(r); has { - checks = append(checks, fmt.Sprintf("%v by %s", r["id"], catalogue.HealthWords(h))) - } - } - // And its own runtime's, where it runs as its own account (novox/hq ADR 0366). - if own, has := catalogue.OwnRuntimeHealth(m); has { - if h, _, _ := catalogue.ReadHealth(own); h.Kind != "" { - checks = append(checks, fmt.Sprintf("%v by %s", own["id"], catalogue.HealthWords(h))) - } - } - if len(checks) > 0 { - fmt.Fprintf(out, ", ready: %s", strings.Join(checks, "; ")) - } - if missing := catalogue.UnsaidTrust(m); len(missing) > 0 { - fmt.Fprintf(out, "; WARNING: %s ask(s) for a setting and do(es) not say whether it is trusted, so it counts as "+ - "trusted: set at the terminal alone; say %q false where nothing trusts it (novox/hq issue 339)", - strings.Join(missing, ", "), catalogue.TrustedField) - } - if missing := catalogue.Undeclared(m); len(missing) > 0 { - fmt.Fprintf(out, "; WARNING: %s stay(s) up and say(s) not how it is ready — judged by liveness alone, "+ - "refused from %s (ADR 0240 rule 8)", strings.Join(missing, ", "), catalogue.HealthRequiredFrom.Format("2006-01-02")) - } - fmt.Fprintln(out) - } - // The count the catalogue keeps (ADR 0240 rule 8), in a line its merge check reads. - fmt.Fprintf(out, "%s %d\n", UndeclaredHealthLine, undeclared) - fmt.Fprintf(out, "%s %d\n", UnsaidTrustLine, unsaid) - if failed > 0 { - return fmt.Errorf("%d problem(s) in %d manifest(s)", failed, len(paths)) - } - fmt.Fprintf(out, "%d manifest(s) checked. Judged against the seats this binary carries; a claim on "+ - "one of the mesh's own seats is judged fully at registration, and a seat declared by a "+ - "module not given here reads as unknown. Identities judged on a %d-character machine name, "+ - "against the bounds of the providers given here\n", len(paths), longestMachine) - return nil -} - -// UndeclaredHealthLine starts the line `module check` says the count of long-running resources without -// `health` in, over the manifests given: the catalogue's merge check compares it with the number it keeps. -const UndeclaredHealthLine = "long-running resources without health:" - -// UnsaidTrustLine starts the line `module check` says the count of files that ask for a setting and do not say -// whether it is trusted, and so count as trusted (novox/hq issue 339). -const UnsaidTrustLine = "files asking for a setting without saying whether it is trusted:" - -// checkNow is the clock `module check` judges the date by; a test sets it. -var checkNow = time.Now - -func joinInvokes(invokes []string) string { - if len(invokes) == 1 && invokes[0] == "*" { - return "every tool" - } - s := "" - for i, t := range invokes { - if i > 0 { - s += ", " - } - s += t - } - return s -} - -// manifestsUnder lists every module.json below a directory, for `module check `. -func manifestsUnder(dir string) ([]string, error) { - var found []string - err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() && (d.Name() == "node_modules" || d.Name() == ".git" || d.Name() == "dist") { - return filepath.SkipDir - } - if !d.IsDir() && d.Name() == "module.json" { - found = append(found, path) - } - return nil - }) - sort.Strings(found) - return found, err -} diff --git a/cmd/mesh-controller/check_test.go b/cmd/mesh-controller/check_test.go index 6d323986..eaa088b5 100644 --- a/cmd/mesh-controller/check_test.go +++ b/cmd/mesh-controller/check_test.go @@ -1,80 +1,13 @@ package main import ( - "bytes" - "os" - "path/filepath" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" - "github.com/novox/mesh-controller/internal/beside" ) -// The check anybody can run is the check registration runs (novox/hq issue 148, ADR 0037): a manifest -// with a known fault is named, and one without passes, with no store opened. -func TestModuleCheckNamesAFaultAndNeedsNoMesh(t *testing.T) { - dir := t.TempDir() - good := filepath.Join(dir, "good.json") - bad := filepath.Join(dir, "bad.json") - os.WriteFile(good, []byte(`{"module":"shop","version":"1","tools":["price"],"invokes":["mesh-catalog.catalog_modules"]}`), 0o600) - os.WriteFile(bad, []byte(`{"module":"till","version":"1","invokes":["shop"]}`), 0o600) - - var out bytes.Buffer - if err := moduleCheck([]string{good}, &out); err != nil { - t.Fatalf("a sound manifest was refused: %v\n%s", err, out.String()) - } - if !strings.Contains(out.String(), "shop: ok, 1 tool(s), invokes mesh-catalog.catalog_modules") { - t.Fatalf("the report does not say what it checked:\n%s", out.String()) - } - - out.Reset() - err := moduleCheck([]string{good, bad}, &out) - if err == nil { - t.Fatal("a manifest invoking a module and no tool passed") - } - if !strings.Contains(out.String(), `till invokes "shop", which does not name a tool`) { - t.Fatalf("the fault is not named in the manifest's words:\n%s", out.String()) - } -} - -// The rules between manifests run over what was given together: a seat two modules declare is -// refused, which no single-manifest check can see. -func TestModuleCheckJudgesBetweenTheManifestsGiven(t *testing.T) { - dir := t.TempDir() - a := filepath.Join(dir, "a.json") - b := filepath.Join(dir, "b.json") - os.WriteFile(a, []byte(`{"module":"a","version":"1","seats":[{"name":"printer","scope":"mesh"}]}`), 0o600) - os.WriteFile(b, []byte(`{"module":"b","version":"1","seats":[{"name":"printer","scope":"mesh"}]}`), 0o600) - var out bytes.Buffer - if err := moduleCheck([]string{a, b}, &out); err == nil { - t.Fatalf("two declarations of one seat passed:\n%s", out.String()) - } - if !strings.Contains(out.String(), "a seat name means one protocol") { - t.Fatalf("the cross-manifest rule was not the one named:\n%s", out.String()) - } -} - -// The real catalogue passes the command, the way it passes the test that used to be the only check: the catalogue this -// controller is released with, captured in testdata/beside at the commit CAPTURED names. Not the clone a merge check puts -// beside, which is the catalogue's trunk even in a delivery group holding a catalogue pull request (ADR 0484 clause 4): -// a controller whose check reads a word that group's catalogue brings would fail on the trunk's catalogue before the -// group merges both (issue 774: the release's sdk refuses an offer that states no credential form, and the catalogue's -// trunk states none until the release's catalogue merges). Moving CAPTURED to the catalogue a change is released with is -// part of that change. -func TestModuleCheckPassesTheCatalogue(t *testing.T) { - root := filepath.Join(beside.CapturedDir(t, "mesh-catalog"), "modules") - paths, err := manifestsUnder(root) - if err != nil || len(paths) == 0 { - t.Fatalf("no manifests under %s: %v", root, err) - } - var out bytes.Buffer - if err := moduleCheck(paths, &out); err != nil { - t.Fatalf("the catalogue does not pass its own check: %v\n%s", err, out.String()) - } -} - func TestRegistrationRefusesADefinitionNamingAnInstallation(t *testing.T) { // novox/hq ADR 0155: the check moves to registration once the catalogue passes it. Both // ways in — `module add` and a build's result — go through this, and a name declared on @@ -97,71 +30,3 @@ func TestRegistrationRefusesADefinitionNamingAnInstallation(t *testing.T) { t.Fatalf("a name declared on purpose passes; got %v", err) } } - -// **The controller's own manifest names every verb of its seat** (novox/hq ADR 0132): its tools lagged -// the seat's verbs for weeks, and `module check` — the gate's first step for a change touching it — -// refused it. Held here, so a verb added to the table without the manifest fails this repository's -// own suite rather than its next pull request's gate. -func TestTheControllersManifestServesEveryVerbOfItsSeat(t *testing.T) { - var out strings.Builder - if err := moduleCheck([]string{"../../module.json"}, &out); err != nil { - t.Fatalf("the controller's own module.json fails module check: %v\n%s", err, out.String()) - } -} - -// A module that uses a seat another module declares (novox/hq issue 364): refused over the whole catalogue when the -// declarer is missing, a note when the gate says it gives only the manifests a change touches. -func TestASeatAnotherModuleDeclaresIsANoteOverSomeManifests(t *testing.T) { - dir := t.TempDir() - user := filepath.Join(dir, "user.json") - os.WriteFile(user, []byte(`{"module":"asker","version":"1","uses":["operator-channel"]}`), 0o600) - var out bytes.Buffer - if err := moduleCheck([]string{user}, &out); err == nil { - t.Fatalf("a use of a seat nothing given declares passed the whole-catalogue check:\n%s", out.String()) - } - t.Setenv(SomeManifestsVar, "1") - out.Reset() - if err := moduleCheck([]string{user}, &out); err != nil { - t.Fatalf("over some manifests the use was refused:\n%s", out.String()) - } - if !strings.Contains(out.String(), "note: asker uses the seat \"operator-channel\"") { - t.Fatalf("the note was not said:\n%s", out.String()) - } -} - -// **A module lives whole in one repository** (novox/hq ADR 0420 §1, issue 1019): `module check`, told the -// repository it runs for, refuses an artifact whose build context names another; run by hand, it does not know. -func TestModuleCheckRefusesAnArtifactContextNamingAnotherRepository(t *testing.T) { - dir := t.TempDir() - write := func(name, context string) string { - path := filepath.Join(dir, name+".json") - raw := `{"module":"` + name + `","version":"1","build":{"artifacts":[{"name":"server","kind":"image","from":"Dockerfile"` + context + `}]}}` - if err := os.WriteFile(path, []byte(raw), 0o600); err != nil { - t.Fatal(err) - } - return path - } - other := write("other", `,"context":{"seat":"git","repository":"novox/mesh-controller","ref":"main"}`) - own := write("own", `,"context":{"seat":"git","repository":"novox/mesh-catalog","ref":"main"}`) - none := write("none", ``) - - t.Setenv("MESH_CHECK_REPOSITORY", "novox/mesh-catalog") - var out bytes.Buffer - if err := moduleCheck([]string{other}, &out); err == nil { - t.Fatalf("a context naming another repository passed:\n%s", out.String()) - } - if !strings.Contains(out.String(), "other builds artifact server from novox/mesh-controller: a module lives whole in one repository (novox/hq ADR 0420 §1)") { - t.Errorf("the refusal is not in the words of the gate:\n%s", out.String()) - } - for _, path := range []string{own, none} { - out.Reset() - if err := moduleCheck([]string{path}, &out); err != nil { - t.Errorf("%s was refused: %v\n%s", path, err, out.String()) - } - } - t.Setenv("MESH_CHECK_REPOSITORY", "") - out.Reset() - if err := moduleCheck([]string{other}, &out); err != nil { - t.Errorf("a local run, with no repository named, refused it: %v\n%s", err, out.String()) - } -} diff --git a/cmd/mesh-controller/health_check_test.go b/cmd/mesh-controller/health_check_test.go deleted file mode 100644 index 4c4ddf53..00000000 --- a/cmd/mesh-controller/health_check_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// Every catalogue module that runs something long-lived declares how it is ready (novox/hq ADR 0240 rule -// 8): `module check` warns and counts the undeclared before the date, and refuses them from it. -func TestModuleCheckCountsTheUndeclaredAndRefusesThemFromTheDate(t *testing.T) { - dir := t.TempDir() - digest := "@sha256:" + strings.Repeat("a", 64) - path := filepath.Join(dir, "module.json") - os.WriteFile(path, []byte(`{"module":"web","listens":[{"name":"web","port":80,"from":"mesh"}],"resources":[ - {"id":"server","type":"container","name":"web","image":"registry.example/web`+digest+`","ports":["80"], - "health":{"kind":"http","endpoint":"web"}}, - {"id":"worker","type":"container","name":"web-worker","image":"registry.example/web`+digest+`"}, - {"id":"seed","type":"container","name":"web-seed","image":"registry.example/web`+digest+`","run-once":true}]}`), 0o600) - defer func() { checkNow = time.Now }() - - checkNow = func() time.Time { return catalogue.HealthRequiredFrom.Add(-time.Hour) } - var out bytes.Buffer - if err := moduleCheck([]string{path}, &out); err != nil { - t.Fatalf("refused before the date: %v\n%s", err, out.String()) - } - for _, want := range []string{"ready: server by http / on web every 30s", "WARNING: worker stay(s) up", - catalogue.HealthRequiredFrom.Format("2006-01-02"), UndeclaredHealthLine + " 1"} { - if !strings.Contains(out.String(), want) { - t.Errorf("the check does not say %q:\n%s", want, out.String()) - } - } - - checkNow = func() time.Time { return catalogue.HealthRequiredFrom } - out.Reset() - if err := moduleCheck([]string{path}, &out); err == nil { - t.Fatalf("a long-running resource without health passed after the date:\n%s", out.String()) - } - if !strings.Contains(out.String(), "web: worker stays up and does not say how it is ready") { - t.Errorf("the refusal does not name the resource:\n%s", out.String()) - } -} - -// A file that asks for a setting without saying whether it is trusted counts as trusted (novox/hq issue 339): -// `module check` lists and counts it, and never refuses it — there is nothing unsafe to refuse. -func TestModuleCheckListsUnmarkedFilesAndNeverRefusesThem(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "module.json") - os.WriteFile(path, []byte(`{"module":"power","resources":[ - {"id":"logind","type":"file","path":"/etc/systemd/logind.conf.d/power.conf","mode":"0644","trusted":true, - "content":"HandleLidSwitch=${setting:lid}\n"}, - {"id":"note","type":"file","path":"/var/lib/power/note","mode":"0644","content":"${setting:greeting}\n"}]}`), 0o600) - defer func() { checkNow = time.Now }() - for _, at := range []time.Time{time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), time.Date(2036, 1, 1, 0, 0, 0, 0, time.UTC)} { - checkNow = func() time.Time { return at } - var out bytes.Buffer - if err := moduleCheck([]string{path}, &out); err != nil { - t.Fatalf("refused at %v: %v\n%s", at, err, out.String()) - } - for _, want := range []string{"note ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted", - UnsaidTrustLine + " 1"} { - if !strings.Contains(out.String(), want) { - t.Errorf("the check does not say %q:\n%s", want, out.String()) - } - } - } -} diff --git a/cmd/mesh-controller/identity_bound_test.go b/cmd/mesh-controller/identity_bound_test.go deleted file mode 100644 index d8a5e0c9..00000000 --- a/cmd/mesh-controller/identity_bound_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" -) - -// novox/hq ADR 0225, issue 263: a consumer's identity is bounded by the provision it requires, an -// overflow is refused before merge by `module check`, and a provider's machine is never refused for -// one consumer's identity. - -// `module check` refuses the pull request that introduces an overflow, naming the module. -func TestModuleCheckRefusesAnIdentityThatOverflowsWhatItRequires(t *testing.T) { - dir := t.TempDir() - write := func(name, body string) string { - p := filepath.Join(dir, name+".json") - if err := os.WriteFile(p, []byte(body), 0o600); err != nil { - t.Fatal(err) - } - return p - } - objects := write("objects", `{"module":"objects","version":"1", - "provides":[{"name":"s3-bucket","scope":"mesh","identity":{"max":20,"in":"an S3 access key"},"credential":{"form":"pair"}}], - "receives":{"s3-bucket":"/var/lib/mesh/objects/mesh.json"}}`) - resolver := write("resolver", `{"module":"resolver","version":"1", - "provides":[{"name":"wildcard-resolution","scope":"mesh","identity":false,"credential":{"form":"none"}}]}`) - album := write("photoalbum", `{"module":"photoalbum","version":"1","requires":["s3-bucket"]}`) - nm := write("networkmanager", `{"module":"networkmanager","version":"1","requires":["wildcard-resolution"]}`) - - var out bytes.Buffer - if err := moduleCheckFor([]string{resolver, nm}, 6, &out); err != nil { - t.Fatalf("a long name requiring a keyless provision was refused (issue 263): %v\n%s", err, out.String()) - } - out.Reset() - err := moduleCheckFor([]string{objects, album, resolver, nm}, 6, &out) - if err == nil { - t.Fatalf("an identity overflowing an S3 access key passed:\n%s", out.String()) - } - if !strings.Contains(out.String(), "photoalbum wants s3-bucket") || - !strings.Contains(out.String(), "`slug` of at most 8 characters") || - strings.Contains(out.String(), "networkmanager wants") { - t.Fatalf("the refusal does not name the one overflowing module and its remedy:\n%s", out.String()) - } -} - -// Tonight's case, through the commands: networkmanager on a six-character machine requires the -// resolver provision, and a second consumer there overflows an object store's access key. The -// provider's machine still composes; the overflowing consumer is left out of its grants and named, -// by push and by `status`, and the keyless consumer is granted with its long name. diff --git a/cmd/mesh-controller/kept_helpers.go b/cmd/mesh-controller/kept_helpers.go index c4291b15..44781ce6 100644 --- a/cmd/mesh-controller/kept_helpers.go +++ b/cmd/mesh-controller/kept_helpers.go @@ -219,15 +219,3 @@ func theGraph(ctx context.Context, inv *inventory.Inventory) ([]inventory.Entry, } return entries, read, edges, nil } - -// refusedWord is the word a build's failure says the reader did not know; empty when it says none. -func refusedWord(failed string) string { - if m := unknownWord.FindStringSubmatch(failed); m != nil { - return m[1] - } - return "" -} - -// unknownWord is how the strict reading of a manifest says a word is not one it knows: encoding/json's own words, which -// the build-agent's answer carries whole. The build's answer has no typed field for it (Failed is the builder's words). -var unknownWord = regexp.MustCompile(`json: unknown field "([^"]+)"`) diff --git a/cmd/mesh-controller/key_takein.go b/cmd/mesh-controller/key_takein.go index abb3a88b..ebf8caa2 100644 --- a/cmd/mesh-controller/key_takein.go +++ b/cmd/mesh-controller/key_takein.go @@ -51,6 +51,8 @@ var errKeyFailed = errors.New("the builder's key failed") type keyReads interface { Build(ctx context.Context, module, fp string) (builderkeys.Key, error) Manifest(ctx context.Context, digest string) (sdkdelivery.Manifest, error) + // Verdict is the indexer's verdict on the manifest of digest, nil when it wrote none (taken as accepted). + Verdict(ctx context.Context, digest string) (*sdkdelivery.ManifestVerdict, error) } // keyBuildID is the id a key's build is recorded under: the key and its revision, so the same revision recorded twice @@ -335,16 +337,26 @@ func buildOfKey(ctx context.Context, keys keyReads, t keyTake, kc keyContext) ( if err != nil { return waits("%s's manifest: %v", t.Module, err) } - manifest, err := catalogue.ParseManifestWith([]byte(src.JSON), kc.list) + // **The controller judges no manifest; the indexer did, once** (ADR 0578 §3, §6): the manifest is read leniently, and + // the indexer's verdict on its digest is taken. A refused one is held and named, never registered; a word this + // controller does not read is set aside and named, and the rest is registered with no second build. + manifest, err := catalogue.Decode([]byte(src.JSON)) if err != nil { - // **A word the running controller does not read waits for the controller that reads it** (ADR 0501 §7): the - // key stays done, and the controller that reads the word registers it with no second build. - if word := refusedWord(err.Error()); word != "" { - return waits("%s's manifest uses the word %q, which this controller does not read: it is registered "+ - "when a controller that reads it runs", t.Module, word) - } return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the builder built %s and its manifest is "+ - "refused: %w", t.Module, err) + "not read: %w", t.Module, err) + } + verdict, err := keys.Verdict(ctx, k.Build.Manifest) + if err != nil { + return waits("%s's verdict: %v", t.Module, err) + } + if refused, _ := sdkdelivery.TakeManifestVerdict(verdict); refused { + return waits("%s is held: %s", t.Module, verdict.Why()) + } + if manifest.ReadNoFurther() { + return waits("%s is held: %s", t.Module, manifest.SetAside()) + } + if aside := manifest.SetAside(); aside != "" { + fmt.Printf("%s's manifest has a field this controller does not read, set aside: %s\n", t.Module, aside) } if manifest.Module != t.Module { return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the key of %s names the manifest of %s", diff --git a/cmd/mesh-controller/key_takein_test.go b/cmd/mesh-controller/key_takein_test.go index 37928d49..6ea232ee 100644 --- a/cmd/mesh-controller/key_takein_test.go +++ b/cmd/mesh-controller/key_takein_test.go @@ -23,6 +23,14 @@ import ( type keyBuckets struct { keys map[string]builderkeys.Key manifests map[string]string + verdicts map[string]sdkdelivery.ManifestVerdict // by manifest digest; none is accepted +} + +func (k keyBuckets) Verdict(_ context.Context, digest string) (*sdkdelivery.ManifestVerdict, error) { + if v, ok := k.verdicts[digest]; ok { + return &v, nil + } + return nil, nil } func (k keyBuckets) Build(_ context.Context, module, fp string) (builderkeys.Key, error) { @@ -148,15 +156,36 @@ func TestAKeyNotReadyWaitsAndAFailedKeyFails(t *testing.T) { } } -// ADR 0501 §7: a manifest word this controller does not read waits for the controller that reads it; never fails. -func TestAManifestWordTheControllerDoesNotReadWaits(t *testing.T) { - raw := `{"module":"x","version":"1","not-a-word-yet":true,"build":{"artifacts":[{"name":"a","kind":"archive","from":"files"}]}, +// ADR 0578 §3, §6 (changing ADR 0501 §7): the controller judges no manifest. A word it does not read is set aside and the +// module registered with no second build; the module the indexer's verdict refuses is held and named, never failed. +func TestAKeyIsTakenInOnTheIndexersVerdictAndNeverJudgedHere(t *testing.T) { + const tail = `"build":{"artifacts":[{"name":"a","kind":"archive","from":"files"}]}, "resources":[{"id":"a","type":"archive","path":"/opt/x","artifact":"a"}]}` - keys, trunk := aKeyOf(raw, 1, sdkdelivery.BuiltArtifact{Name: "a", Kind: "archive", Digest: shaOf("a"), - Reference: "artifact-store://x/a/blobs/" + shaOf("a")}) - _, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{}) - if !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), `"not-a-word-yet"`) { - t.Fatalf("an unread word: %v", err) + refusal := func(digest string) map[string]sdkdelivery.ManifestVerdict { + return map[string]sdkdelivery.ManifestVerdict{digest: {Module: "x", State: sdkdelivery.VerdictRefused, + Problems: []string{"x names a seat nothing declares"}}} + } + for _, row := range []struct { + name, raw string + refused bool + holds string + }{ + {"an unread word", `{"module":"x","version":"1","not-a-word-yet":true,` + tail, false, ""}, + {"no verdict is accepted", `{"module":"x","version":"1",` + tail, false, ""}, + {"a refusal is held and named", `{"module":"x","version":"1",` + tail, true, "names a seat nothing declares"}, + } { + keys, trunk := aKeyOf(row.raw, 1, sdkdelivery.BuiltArtifact{Name: "a", Kind: "archive", Digest: shaOf("a"), + Reference: "artifact-store://x/a/blobs/" + shaOf("a")}) + if row.refused { + keys.verdicts = refusal(sdkdelivery.ManifestDigest([]byte(row.raw))) + } + _, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{}) + switch { + case row.holds == "" && err != nil: + t.Errorf("%s: %v", row.name, err) + case row.holds != "" && (!errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), row.holds)): + t.Errorf("%s: %v", row.name, err) + } } } diff --git a/cmd/mesh-controller/main.go b/cmd/mesh-controller/main.go index 7f3337f1..ca6a950e 100644 --- a/cmd/mesh-controller/main.go +++ b/cmd/mesh-controller/main.go @@ -223,7 +223,6 @@ var usageText = `mesh-controller — the control plane overlay place [flags] say where a node is and how it is reached overlay show the private network, as the mesh computes it module add register a module from its manifest - module check ... judge manifests where they are written, with no mesh (exit 1 on any problem) module list what modules this mesh knows about module moved the source has a newer commit than the mesh built module forget remove one, unless a node runs it or the mesh holds things for it diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index c355e753..87eefe0c 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -64,35 +64,7 @@ var provided = providedModules() func moduleCommand(ctx context.Context, args []string) error { if len(args) == 0 { - return errors.New("module add , module check ..., module list, or module forget ") - } - // `check` needs no mesh, and must not: it is what somebody runs in their own repository before - // there is a mesh in reach (novox/hq issue 148). A directory expands to every manifest under it. - if args[0] == "check" { - set := flag.NewFlagSet("module check", flag.ContinueOnError) - // The longest machine name an identity must fit on (novox/hq ADR 0225): a mesh passes its own. - longest := set.Int("longest-machine-name", catalogue.DefaultLongestMachine, - "judge each module's identity on a machine name this many characters long") - given, err := parseAround(set, args[1:]) - if err != nil { - return err - } - if *longest < 1 { - return errors.New("--longest-machine-name is a length, at least 1") - } - var paths []string - for _, a := range given { - if info, err := os.Stat(a); err == nil && info.IsDir() { - under, err := manifestsUnder(a) - if err != nil { - return err - } - paths = append(paths, under...) - continue - } - paths = append(paths, a) - } - return moduleCheckFor(paths, *longest, os.Stdout) + return errors.New("module add , module list, or module forget ") } open, err := openStores(ctx) if err != nil { @@ -126,7 +98,7 @@ func moduleCommand(ctx context.Context, args []string) error { if err != nil { return err } - m, err := catalogue.ParseManifest(raw) + m, err := catalogue.Decode(raw) if err != nil { return err } diff --git a/cmd/mesh-controller/predefined_test.go b/cmd/mesh-controller/predefined_test.go index c1c1ecde..4653dbf4 100644 --- a/cmd/mesh-controller/predefined_test.go +++ b/cmd/mesh-controller/predefined_test.go @@ -1,10 +1,7 @@ package main import ( - "bytes" "encoding/json" - "os" - "path/filepath" "strings" "testing" @@ -12,8 +9,8 @@ import ( "github.com/novox/mesh-controller/internal/link" ) -// The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661): `module check` and the -// merge gate judge each manifest against the list at the root of its tree; registration publishes the newest +// The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661): the +// merge gate judges each manifest against the list at the root of its tree; registration publishes the newest // trunk build's list of the catalogue and judges every module against the list the mesh publishes. const theFirstKeys = `{ @@ -41,60 +38,6 @@ func withoutAPublishedList(t *testing.T) { t.Cleanup(func() { catalogue.ReplacePublished(was) }) } -// aRepositoryTree is a repository's checkout holding one module at modules/notifier, and the list when given. -func aRepositoryTree(t *testing.T, manifest string, list *string) string { - t.Helper() - root := t.TempDir() - if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { - t.Fatal(err) - } - if list != nil { - if err := os.WriteFile(filepath.Join(root, catalogue.PredefinedFile), []byte(*list), 0o644); err != nil { - t.Fatal(err) - } - } - return aTemplatedModule(t, filepath.Join(root, "modules", "notifier"), manifest, nil) -} - -func TestModuleCheckJudgesAManifestAgainstTheListAtItsRepositoryRoot(t *testing.T) { - withoutAPublishedList(t) - list := theFirstKeys - check := func(path string) (string, error) { - var out bytes.Buffer - err := moduleCheck([]string{path}, &out) - return out.String(), err - } - if out, err := check(aRepositoryTree(t, notifierFallingBackTo("font-size", "number"), &list)); err != nil || - !strings.Contains(out, "notifier: ok") { - t.Fatalf("a module naming font-size was refused: %v\n%s", err, out) - } - for _, c := range []struct { - manifest string - list *string - says string - }{ - {notifierFallingBackTo("font-scale", "number"), &list, `"font-scale", which is not on the list`}, - {notifierFallingBackTo("font-size", "string"), &list, "which is a number, and this value is a string"}, - {notifierFallingBackTo("font-size", "number"), nil, "is not present"}, - {notifierFallingBackTo("font-size", "number"), ptr(`{"font-size": {"type": "number"}}`), "no description"}, - } { - if out, err := check(aRepositoryTree(t, c.manifest, c.list)); err == nil || !strings.Contains(out, c.says) { - t.Errorf("wanted the check refused saying %q, got %v:\n%s", c.says, err, out) - } - } -} - -func ptr(s string) *string { return &s } - -// writeTheList puts the list at the root of a tree the gate judges. -func writeTheList(t *testing.T, tree, list string) string { - t.Helper() - if err := os.WriteFile(filepath.Join(tree, catalogue.PredefinedFile), []byte(list), 0o644); err != nil { - t.Fatal(err) - } - return tree -} - // catalogueBuild is a trunk build of the catalogue carrying the list at a generation. func catalogueBuild(id, manifest, list string, generation int) link.BuildResult { r := link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git", diff --git a/cmd/mesh-controller/shares_test.go b/cmd/mesh-controller/shares_test.go index 294a38e8..3d5589c0 100644 --- a/cmd/mesh-controller/shares_test.go +++ b/cmd/mesh-controller/shares_test.go @@ -1,14 +1,10 @@ package main import ( - "bytes" "crypto/ecdh" "crypto/rand" "encoding/base64" "encoding/json" - "os" - "path/filepath" - "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -79,26 +75,3 @@ func TestTheProvidersMachineReceivesTheSharesOfAnS3BucketRequirement(t *testing. t.Fatalf("the provider's grant for the consumer carries shares %s, want %s", got, want) } } - -// 1593/AC2 unit: the module check accepts a manifest with shares and refuses an unknown method, citing ADR 0468 §8. -func TestModuleCheckAcceptsSharesAndRefusesAnUnknownMethod(t *testing.T) { - dir := t.TempDir() - write := func(name, body string) string { - p := filepath.Join(dir, name+".json") - if err := os.WriteFile(p, []byte(body), 0o600); err != nil { - t.Fatal(err) - } - return p - } - var out bytes.Buffer - if err := moduleCheckFor([]string{write("files", filesRequiringShares)}, 6, &out); err != nil { - t.Fatalf("a manifest with shares was refused: %v\n%s", err, out.String()) - } - out.Reset() - bad := write("bad", `{"module":"bad","version":"1","requires":[ - {"name":"s3-bucket","shares":[{"to":"seat:node-files","methods":["delete"]}]}]}`) - err := moduleCheckFor([]string{bad}, 6, &out) - if err == nil || !strings.Contains(out.String(), "ADR 0468 §8") { - t.Fatalf("an unknown method passed or the refusal does not cite the ADR: %v\n%s", err, out.String()) - } -} diff --git a/cmd/mesh-controller/status.go b/cmd/mesh-controller/status.go index a9f86272..703989d4 100644 --- a/cmd/mesh-controller/status.go +++ b/cmd/mesh-controller/status.go @@ -318,7 +318,7 @@ func printStatus(asked answers) error { fmt.Printf(" %-12s %-20s %-22s %q is %d, %s keeps %d\n", o.Consumer, o.Module, o.Provision, o.Identity, len(o.Identity), o.Bound.In, o.Bound.Max) } - fmt.Printf("\n a shorter `slug` in the module's definition fits it; `module check` refuses one before merge\n\n") + fmt.Printf("\n a shorter `slug` in the module's definition fits it\n\n") } // Repairs done by hand this week (novox/hq to-be 45 §7). Not a fault, so it does not break "all diff --git a/cmd/mesh-controller/template_test.go b/cmd/mesh-controller/template_test.go deleted file mode 100644 index d3972043..00000000 --- a/cmd/mesh-controller/template_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" -) - -// A file's text kept as a template file in its module's repository (novox/hq ADR 0349 §2, design 57 package T1, -// issue 654): `module check` and the merge gate read it from the tree beside the manifest, so a change to a -// template alone is judged as a change to its module. - -const templatedNotifier = `{"module":"notifier","version":"1", - "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, - "resources":[{"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","template":"files/notifier.conf"}]}` - -// inlineNotifier is the same module as the mesh holds it: built, its text inline. -const inlineNotifier = `{"module":"notifier","version":"1", - "resources":[{"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","content":"volume = 3\n"}]}` - -// aTemplatedModule writes a module's directory: its manifest and its files/. -func aTemplatedModule(t *testing.T, dir, manifest string, files map[string]string) string { - t.Helper() - if err := os.MkdirAll(dir, 0o755); err != nil { - t.Fatal(err) - } - path := filepath.Join(dir, "module.json") - if err := os.WriteFile(path, []byte(manifest), 0o644); err != nil { - t.Fatal(err) - } - for name, text := range files { - full := filepath.Join(dir, filepath.FromSlash(name)) - if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(full, []byte(text), 0o644); err != nil { - t.Fatal(err) - } - } - return path -} - -func TestModuleCheckReadsTemplatesFromTheTreeBesideTheManifest(t *testing.T) { - good := aTemplatedModule(t, t.TempDir(), templatedNotifier, map[string]string{"files/notifier.conf": "volume = 3\n"}) - var out bytes.Buffer - if err := moduleCheck([]string{good}, &out); err != nil || !strings.Contains(out.String(), "notifier: ok") { - t.Fatalf("a module with its text in files/ was refused: %v\n%s", err, out.String()) - } - - // A change to the template alone, to text the controller refuses inline, is refused the same. - bad := aTemplatedModule(t, t.TempDir(), templatedNotifier, - map[string]string{"files/notifier.conf": "volume = 3\nstore = ${dir:nowhere}\n"}) - out.Reset() - if err := moduleCheck([]string{bad}, &out); err == nil || !strings.Contains(out.String(), "nowhere") { - t.Fatalf("a template whose text is refused inline passed:\n%s", out.String()) - } - - // A symlink under files/ that no template names: the build's packing refuses it, so the check does too. - linked := t.TempDir() - aTemplatedModule(t, linked, templatedNotifier, map[string]string{"files/notifier.conf": "volume = 3\n"}) - if err := os.Symlink("/etc/hostname", filepath.Join(linked, "files", "elsewhere")); err != nil { - t.Fatal(err) - } - out.Reset() - if err := moduleCheck([]string{filepath.Join(linked, "module.json")}, &out); err == nil || - !strings.Contains(out.String(), "files/elsewhere is a symlink") { - t.Fatalf("a symlink under files/ passed the check:\n%s", out.String()) - } - - // And a template the tree lacks, by name. - missing := aTemplatedModule(t, t.TempDir(), templatedNotifier, nil) - out.Reset() - if err := moduleCheck([]string{missing}, &out); err == nil || !strings.Contains(out.String(), "files/notifier.conf") { - t.Fatalf("a missing template was not refused by name:\n%s", out.String()) - } -} - -// templatedMesh is catalogueMesh with the notifier on the laptop, registered as built: its text inline. - -// What the gate composes for the module in its template form is what the mesh composes for it inline. - -// A module whose files/ is unpacked onto the machine names its own templates directory (issue 731): `module check` -// reads its templates from there and says of it exactly what it says of the same module with its text inline. -func TestModuleCheckReadsTemplatesFromTheTemplatesDirectoryTheManifestNames(t *testing.T) { - const inline = `{"module":"notifier","version":"1", - "build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}]}, - "resources":[{"id":"scripts","type":"archive","path":"/usr/local/lib/notifier","artifact":"scripts"}, - {"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","content":"volume = 3\n"}]}` - const templated = `{"module":"notifier","version":"1","templates":"templates", - "build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}, - {"name":"templates","kind":"archive","from":"templates"}]}, - "resources":[{"id":"scripts","type":"archive","path":"/usr/local/lib/notifier","artifact":"scripts"}, - {"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","template":"templates/notifier.conf"}]}` - scripts := map[string]string{"files/bin/notify": "#!/bin/sh\n"} - var want bytes.Buffer - if err := moduleCheck([]string{aTemplatedModule(t, t.TempDir(), inline, scripts)}, &want); err != nil { - t.Fatalf("the inline module was refused: %v\n%s", err, want.String()) - } - with := map[string]string{"files/bin/notify": "#!/bin/sh\n", "templates/notifier.conf": "volume = 3\n"} - var got bytes.Buffer - if err := moduleCheck([]string{aTemplatedModule(t, t.TempDir(), templated, with)}, &got); err != nil { - t.Fatalf("a module with its text in templates/ was refused: %v\n%s", err, got.String()) - } - if got.String() != want.String() { - t.Fatalf("module check says otherwise of the templated module:\ninline: %s\ntemplated: %s", want.String(), - got.String()) - } - - // Its text in files/ instead, where the machine unpacks it, is not where its manifest says its templates are. - misplaced := strings.Replace(templated, `"templates/notifier.conf"`, `"files/notifier.conf"`, 1) - got.Reset() - if err := moduleCheck([]string{aTemplatedModule(t, t.TempDir(), misplaced, - map[string]string{"files/notifier.conf": "volume = 3\n"})}, &got); err == nil || - !strings.Contains(got.String(), "files/notifier.conf") { - t.Fatalf("a template outside the templates directory the manifest names passed:\n%s", got.String()) - } -} - -// Issue 876: the gate judges a moved module that names its templates as registration reads it after a real build, -// handed its templates; it refused every such module as "marked moved and names no template". This calls the gate's -// own shelf (shelfWithChange), which needs no database. diff --git a/cmd/mesh-controller/unknown_fields.go b/cmd/mesh-controller/unknown_fields.go index e5871a4e..9279f075 100644 --- a/cmd/mesh-controller/unknown_fields.go +++ b/cmd/mesh-controller/unknown_fields.go @@ -20,11 +20,12 @@ const ( ) // unknownFieldObservations is one condition for each module of the catalogue whose stored manifest has -// a key this controller does not know. +// a key this controller does not know and read no further than its name and version (ADR 0578 §6: a manifest read past +// the key is registered and the key named, so it raises nothing). func unknownFieldObservations(known map[string]catalogue.Manifest) []conditions.Observation { names := make([]string, 0, len(known)) for name, m := range known { - if m.UnknownField() != "" { + if m.ReadNoFurther() { names = append(names, name) } } diff --git a/cmd/mesh-controller/unknown_fields_test.go b/cmd/mesh-controller/unknown_fields_test.go index 46af22ea..6e50aa1d 100644 --- a/cmd/mesh-controller/unknown_fields_test.go +++ b/cmd/mesh-controller/unknown_fields_test.go @@ -11,14 +11,18 @@ import ( // A module whose stored manifest has a key this controller does not know is a condition, in plain // words, until it is read again; every other module raises nothing (novox/hq ADR 0262). func TestAModuleWithAnUnknownFieldIsACondition(t *testing.T) { - var later, now catalogue.Manifest - if err := json.Unmarshal([]byte(`{"module": "dunst", "version": "2", "settings": {}, "a-field-from-later": 1}`), &later); err != nil { + var later, now, aside catalogue.Manifest + if err := json.Unmarshal([]byte(`{"module": "dunst", "version": "2", "tools": "not-a-list", "a-field-from-later": 1}`), &later); err != nil { + t.Fatal(err) + } + // A field set aside with the rest read raises nothing (ADR 0578 §6): the module is registered and the field named. + if err := json.Unmarshal([]byte(`{"module": "sway", "version": "2", "settings": {}, "a-field-from-later": 1}`), &aside); err != nil { t.Fatal(err) } if err := json.Unmarshal([]byte(`{"module": "xorg", "version": "1"}`), &now); err != nil { t.Fatal(err) } - observed := unknownFieldObservations(map[string]catalogue.Manifest{"dunst": later, "xorg": now}) + observed := unknownFieldObservations(map[string]catalogue.Manifest{"dunst": later, "xorg": now, "sway": aside}) if len(observed) != 1 || observed[0].ID != "dunst" || observed[0].Kind != kindUnknownField { t.Fatalf("observed: %+v", observed) } diff --git a/cmd/mesh-controller/written_settings_test.go b/cmd/mesh-controller/written_settings_test.go index 2c5ecfa2..f781cd06 100644 --- a/cmd/mesh-controller/written_settings_test.go +++ b/cmd/mesh-controller/written_settings_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "os" "path/filepath" "strings" @@ -10,8 +9,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// ADR 0326 §3 is judged where a definition is written (novox/hq issue 574): `module check`, and -// namesNoInstallation, which `module add`, a build taken in and the merge gate's registration run. +// ADR 0326 §3 is judged where a definition is written (novox/hq issue 574): namesNoInstallation, which `module add`, a build taken in and the merge gate's registration run. func TestWhereADefinitionIsWrittenADeclaringModuleNamesItsFilesKeys(t *testing.T) { const silent = `{"module":"mail-client","version":"1", "settings":{"prefs":{"kind":"preference","default":"// none","why":"its own defaults until a machine names one"}}, @@ -21,19 +19,7 @@ func TestWhereADefinitionIsWrittenADeclaringModuleNamesItsFilesKeys(t *testing.T "content":"{}\n"%s}]}` dir := t.TempDir() bad := filepath.Join(dir, "bad.json") - good := filepath.Join(dir, "good.json") os.WriteFile(bad, []byte(strings.Replace(silent, "%s", "", 1)), 0o600) - os.WriteFile(good, []byte(strings.Replace(silent, "%s", `,"takes-settings":["accounts"]`, 1)), 0o600) - - var out bytes.Buffer - if err := moduleCheck([]string{bad}, &out); err == nil || - !strings.Contains(out.String(), "mergeable accounts names no takes-settings") { - t.Fatalf("module check passed a declaring module's silent mergeable file: %v\n%s", err, out.String()) - } - out.Reset() - if err := moduleCheck([]string{good}, &out); err != nil { - t.Fatalf("the file naming its keys was refused: %v\n%s", err, out.String()) - } raw, _ := os.ReadFile(bad) m, err := catalogue.ParseManifest(raw) diff --git a/go.mod b/go.mod index f29fb348..b019512d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-controller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 github.com/jackc/pgx/v5 v5.10.0 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/go.sum b/go.sum index 0b60d1bd..638aad5e 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYg git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/builderkeys/builderkeys.go b/internal/builderkeys/builderkeys.go index 036ba35b..c1dff370 100644 --- a/internal/builderkeys/builderkeys.go +++ b/internal/builderkeys/builderkeys.go @@ -255,6 +255,31 @@ func (r Reader) Manifest(ctx context.Context, digest string) (delivery.Manifest, return m, nil } +// Verdict is the indexer's verdict on the manifest of digest (ADR 0578 §3), or nil when it holds none (no bucket yet, +// no key): a reader takes that as accepted (delivery.TakeManifestVerdict). +func (r Reader) Verdict(ctx context.Context, digest string) (*delivery.ManifestVerdict, error) { + key, err := delivery.VerdictsKey(digest) + if err != nil { + return nil, err + } + kv, err := r.bucket(ctx, delivery.VerdictsBucket) + if err != nil || kv == nil { + return nil, err + } + e, err := kv.Get(ctx, key) + if errors.Is(err, jetstream.ErrKeyNotFound) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("the verdict on %s: %w", short(digest), err) + } + v, err := delivery.DecodeManifestVerdict(e.Value()) + if err != nil { + return nil, fmt.Errorf("the verdict on %s is not read: %w", short(digest), err) + } + return &v, nil +} + // Watch calls wake with the module of every key the builder or the indexer writes from now on, until ctx ends: what // wakes a plan waiting on a key, with no timer (ADR 0501; the R4e context's A7). A bucket not there yet is watched once // it is: the watch looks again each time wake would have been called for the other bucket, and at every retry. diff --git a/testdata/beside/CAPTURED b/testdata/beside/CAPTURED index 9fb61a4d..68256a6b 100644 --- a/testdata/beside/CAPTURED +++ b/testdata/beside/CAPTURED @@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself. mesh-catalog f3a57dcc56fb6c198095cafd11d0decff9f720c4 modules/*/module.json, modules/*/files, modules/*/templates, predefined.json mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock -mesh-sdk 2da0de75c3f0087cff9c2f5ef789ca6dc3c59045 conformance/events/module-event.json +mesh-sdk 7b09de1e5502231fcd7e41a61086b44f5d7ad545 conformance/events/module-event.json To move them, from this repository's root, with the two repositories checked out beside it: