Merge pull request 'The controller takes the indexer's verdict instead of judging manifests; route-proxy declares its credential form (ADR 0578, 0579)' (#489) from feat/judge-once into main

This commit was merged in pull request #489.
This commit is contained in:
2026-11-01 07:54:23 +00:00
57 changed files with 250 additions and 1145 deletions
@@ -85,7 +85,7 @@ func TestTheControlPlaneIsToldWhereTheNodePutTheStoreAndTheBroker(t *testing.T)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m, err := catalogue.ParseManifest(raw) m, err := catalogue.Decode(raw)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+5 -14
View File
@@ -5,10 +5,11 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"github.com/novox/mesh-controller/internal/conditions"
"io" "io"
"os" "os"
"github.com/novox/mesh-controller/internal/conditions"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
) )
@@ -127,17 +128,13 @@ type takenBuild struct {
carried func() error carried func() error
} }
// registerJudged judges a recorded build and registers its module, or says why not. // registerJudged registers its module, or says why not.
func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild) (catalogue.Manifest, error) { func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild) (catalogue.Manifest, error) {
manifest, kept := j.manifest, j.kept manifest, kept := j.manifest, j.kept
notRegistered := func(err error) (catalogue.Manifest, error) { notRegistered := func(err error) (catalogue.Manifest, error) {
return manifest, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", j.by, manifest.Module, return manifest, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", j.by, manifest.Module,
short(j.commit), err) short(j.commit), err)
} }
if err := namesNoInstallation(manifest); err != nil {
return manifest, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
j.by, j.what, short(j.commit), err)
}
// **Only from the repository the catalogue builds the module from** (novox/hq ADR 0266): else the trunk // **Only from the repository the catalogue builds the module from** (novox/hq ADR 0266): else the trunk
// below is the trunk of whatever repository was built, which may be one an agent made — and a module named // below is the trunk of whatever repository was built, which may be one an agent made — and a module named
// `sudo` from it would be what the next push sends. Another repository is the operator's, at the terminal. // `sudo` from it would be what the next push sends. Another repository is the operator's, at the terminal.
@@ -150,18 +147,12 @@ func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild)
return notRegistered(err) return notRegistered(err)
} }
} }
// Then the module is judged against the list the mesh publishes, as every manifest read again is: one naming a // The manifest was judged once, by the indexer, and its verdict taken before this (buildOfKey; ADR 0578 §3).
// key that list does not hold, or of another type, is refused here and never enters the catalogue.
if j.carried != nil { if j.carried != nil {
if err := j.carried(); err != nil { if err := j.carried(); err != nil {
return manifest, err return manifest, err
} }
} }
if manifest.NamesPredefined() {
if _, err := catalogue.ParseManifest(kept.Manifest); err != nil {
return notRegistered(fmt.Errorf("judged against the list of predefined keys the mesh publishes, %w", err))
}
}
// **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from // **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from
// what the build the mesh stands on was made from registers that build's artifacts at the new // what the build the mesh stands on was made from registers that build's artifacts at the new
// commit, so no machine is sent a new digest for a source nobody changed — an image is not // commit, so no machine is sent a new digest for a source nobody changed — an image is not
@@ -172,7 +163,7 @@ func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild)
return manifest, err return manifest, err
} }
if stands != "" && stands != kept.ID && len(raw) > 0 { if stands != "" && stands != kept.ID && len(raw) > 0 {
if was, err := catalogue.ParseManifest(raw); err == nil && was.Module == manifest.Module { if was, err := catalogue.Decode(raw); err == nil && was.Module == manifest.Module {
fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n", fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n",
manifest.Module, short(j.commit), stands) manifest.Module, short(j.commit), stands)
manifest = was manifest = was
@@ -49,8 +49,10 @@ func TestABuildHeardIsRecordedAndRegistered(t *testing.T) {
"env": map[string]any{"KC_HOSTNAME": "https://login.mesh-one.be"}}}}) "env": map[string]any{"KC_HOSTNAME": "https://login.mesh-one.be"}}}})
_, _, err = takeIn(ctx, open.inventory, link.BuildResult{ _, _, err = takeIn(ctx, open.inventory, link.BuildResult{
ID: "b-2", Repository: "/r", On: "anchor", Commit: "0123456789", Manifest: named}) ID: "b-2", Repository: "/r", On: "anchor", Commit: "0123456789", Manifest: named})
if err == nil || !strings.Contains(err.Error(), "does not register it") { // Not registered from a source the catalogue does not build it from. That it names an installation is no longer the
t.Fatalf("a definition naming an installation was taken in: %v", err) // controller's to judge (ADR 0578 §3): the indexer's judge refuses it (mesh-delivery-indexer judge's installation rows).
if err == nil || !strings.Contains(err.Error(), "recorded and not registered") {
t.Fatalf("a definition from a source the catalogue does not build it from was taken in: %v", err)
} }
if shelf, _ := open.inventory.Catalogue(ctx); shelf["idp"].Module != "" { if shelf, _ := open.inventory.Catalogue(ctx); shelf["idp"].Module != "" {
t.Fatal("the refused module was registered anyway") t.Fatal("the refused module was registered anyway")
@@ -17,6 +17,8 @@ import (
type builderOnTheBus struct { type builderOnTheBus struct {
js *broker.JetStream js *broker.JetStream
builds, modules, manifests jetstream.KeyValue builds, modules, manifests jetstream.KeyValue
// verdicts is the indexer's state of verdicts on manifests (ADR 0578 §1).
verdicts jetstream.KeyValue
} }
func aBuilderOnTheBus(t *testing.T) builderOnTheBus { func aBuilderOnTheBus(t *testing.T) builderOnTheBus {
@@ -40,6 +42,7 @@ func aBuilderOnTheBus(t *testing.T) builderOnTheBus {
{&b.builds, sdkdelivery.BuilderBuildsBucket, sdkdelivery.BuilderBuildsHistory}, {&b.builds, sdkdelivery.BuilderBuildsBucket, sdkdelivery.BuilderBuildsHistory},
{&b.modules, sdkdelivery.ModulesBucket, sdkdelivery.ModulesHistory}, {&b.modules, sdkdelivery.ModulesBucket, sdkdelivery.ModulesHistory},
{&b.manifests, sdkdelivery.ManifestsBucket, 1}, {&b.manifests, sdkdelivery.ManifestsBucket, 1},
{&b.verdicts, sdkdelivery.VerdictsBucket, sdkdelivery.VerdictsHistory},
} { } {
kv, err := api.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: c.bucket, History: c.history}) kv, err := api.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: c.bucket, History: c.history})
if err != nil { if err != nil {
-337
View File
@@ -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 <manifest.json>... — 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 <dir>`.
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
}
-167
View File
@@ -1,167 +0,0 @@
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
// purpose passes with its reason.
named := catalogue.Manifest{Module: "idp", Resources: []map[string]any{
{"id": "server", "type": "container", "image": "x@sha256:aa",
"env": map[string]any{"KC_HOSTNAME": "https://login.mesh-one.be"}},
}}
err := namesNoInstallation(named)
if err == nil || !strings.Contains(err.Error(), "login.mesh-one.be") ||
!strings.Contains(err.Error(), catalogue.NamesOnPurpose) {
t.Fatalf("a definition naming an installation is refused with the name and the way out; got %v", err)
}
meant := catalogue.Manifest{Module: "site", Resources: []map[string]any{
{"id": "server", "type": "container", "image": "registry.mesh-one.be/org/site@sha256:cc",
catalogue.NamesOnPurpose: map[string]any{
"registry.mesh-one.be": "built outside the mesh until its repository is a build source here"}},
}}
if err := namesNoInstallation(meant); err != nil {
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())
}
}
@@ -11,7 +11,7 @@ import (
// configForm is a module in the config form, shaped as a real one (inventory's config_move_test). // configForm is a module in the config form, shaped as a real one (inventory's config_move_test).
func configForm(t *testing.T, name string) catalogue.Manifest { func configForm(t *testing.T, name string) catalogue.Manifest {
t.Helper() t.Helper()
m, err := catalogue.ParseManifest([]byte(`{"module": "` + name + `", "version": "1", m, err := catalogue.Decode([]byte(`{"module": "` + name + `", "version": "1",
"config": {"level": {"type": "string", "description": "how much it says", "default": "info"}}, "config": {"level": {"type": "string", "description": "how much it says", "default": "info"}},
"resources": [{"id": "conf", "type": "file", "path": "/etc/` + name + `.json", "mode": "0640", "resources": [{"id": "conf", "type": "file", "path": "/etc/` + name + `.json", "mode": "0640",
"content": "{\"level\": \"info\"}", "merge": "json", "takes-config": ["level"]}]}`)) "content": "{\"level\": \"info\"}", "merge": "json", "takes-config": ["level"]}]}`))
+2 -2
View File
@@ -86,8 +86,8 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
} }
} }
// The node's own secrets: a module's superuser password, an administrator, an account. The composition refused what the // The node's own secrets: a module's superuser password, an administrator, an account. A bus credential nobody issued is
// send refuses (a bus credential nobody issued, a given value under an old key). // refused before this (unissuedBusCredential), a given value under an old key by the composition.
for _, one := range d.StoodIn { for _, one := range d.StoodIn {
module, name, ok := strings.Cut(one, "/") module, name, ok := strings.Cut(one, "/")
if !ok || strings.Contains(one, " ") { if !ok || strings.Contains(one, " ") {
+36
View File
@@ -184,6 +184,9 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos
if err := refusedBus(in, node); err != nil { if err := refusedBus(in, node); err != nil {
return sendable{}, nil, err return sendable{}, nil, err
} }
if err := unissuedBusCredential(in, node, d); err != nil {
return sendable{}, nil, err
}
sayFound(ctx, d) sayFound(ctx, d)
out := sendableOf(d) out := sendableOf(d)
out.fileReferences = readsFileReferences(in.Input, node) out.fileReferences = readsFileReferences(in.Input, node)
@@ -212,6 +215,35 @@ func refusedBus(in *meshInput, node string) error {
return nil return nil
} }
// unissuedBusCredential refuses a composition that stands in for a bus credential nobody issued: the module's `broker` own
// secret (its account `<node>.<module>`) or the provisioner client's (`provisioner.<node>`). A bus credential is minted by
// `module issue`, never invented: a send that made one would seal a placeholder its process cannot read (novox/hq issue
// 203). mesh-sdk's composition stands one in, in the Foreseeing mode, since a node's answer to a proposal is asked before
// the vault mints it (mesh-sdk #141, issue 774), so the send refuses it here, in the words the composition used to.
func unissuedBusCredential(in *meshInput, node string, d compose.Declared) error {
for _, one := range d.StoodIn {
module, name, ok := strings.Cut(one, "/")
if !ok || strings.Contains(one, " ") {
continue
}
var user string
switch name {
case "broker":
user = node + "." + module
case compose.ProvisionerSecret:
user = "provisioner." + node
default:
continue
}
if !slices.Contains(in.Input.BusMinted, user) {
return fmt.Errorf("%s on %s has no bus credential: nothing was issued for %s, and a push would seal a "+
"placeholder its process cannot read (novox/hq issue 203). `module issue %s --node %s`, then push again",
module, node, user, module, node)
}
}
return nil
}
// markUnresolvable is an error of a node whose own set does not compose, as planFor marked it. // markUnresolvable is an error of a node whose own set does not compose, as planFor marked it.
func markUnresolvable(err error) error { func markUnresolvable(err error) error {
if compose.IsUnresolvable(err) { if compose.IsUnresolvable(err) {
@@ -256,6 +288,10 @@ func composeSendValidated(ctx context.Context, open *stores, node string) (senda
if err := refusedBus(in, node); err != nil { if err := refusedBus(in, node); err != nil {
return sendable{}, nil, err return sendable{}, nil, err
} }
// Before perform makes what the composition stands in for: a bus credential is never one of them.
if err := unissuedBusCredential(in, node, d); err != nil {
return sendable{}, nil, err
}
if len(byController) > 0 { if len(byController) > 0 {
if d, problems, err = sealedByTheController(in, node, d, byController); err != nil { if d, problems, err = sealedByTheController(in, node, d, byController); err != nil {
return sendable{}, nil, err return sendable{}, nil, err
@@ -78,7 +78,7 @@ func aConfiguredMesh(t *testing.T) (*stores, configuredMesh, map[string]map[stri
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m, err := catalogue.ParseManifest(text) m, err := catalogue.Decode(text)
if err != nil { if err != nil {
t.Fatalf("%s: %v", name, err) t.Fatalf("%s: %v", name, err)
} }
+1 -1
View File
@@ -17,7 +17,7 @@ const n8nWithAGroup = `{"module":"n8n","version":"1",
// 1638/AC1: a node that reports the capability gets the module; one that does not has it held, the capability named. // 1638/AC1: a node that reports the capability gets the module; one that does not has it held, the capability named.
func TestAModuleWithGroupsIsHeldOnANodeThatDoesNotReportTheCapability(t *testing.T) { func TestAModuleWithGroupsIsHeldOnANodeThatDoesNotReportTheCapability(t *testing.T) {
m, err := catalogue.ParseManifest([]byte(n8nWithAGroup)) m, err := catalogue.Decode([]byte(n8nWithAGroup))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -62,7 +62,7 @@ func TestNatsUnassigningIrreplaceableDataRetiresItAndAnEmptyReplacementIsUrgent(
Claims: []catalogue.Claim{{Name: catalogue.BackupSeat, Scope: catalogue.ScopeNode, Serves: []string{"backed-up", "now", "restore"}}}}, built); err != nil { Claims: []catalogue.Claim{{Name: catalogue.BackupSeat, Scope: catalogue.ScopeNode, Serves: []string{"backed-up", "now", "restore"}}}}, built); err != nil {
t.Fatal(err) t.Fatal(err)
} }
house, err := catalogue.ParseManifest([]byte(houseManifest)) house, err := catalogue.Decode([]byte(houseManifest))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -24,7 +24,7 @@ func shelfFor(t *testing.T, manifests ...string) map[string]catalogue.Manifest {
t.Helper() t.Helper()
out := map[string]catalogue.Manifest{} out := map[string]catalogue.Manifest{}
for _, raw := range manifests { for _, raw := range manifests {
m, err := catalogue.ParseManifest([]byte(raw)) m, err := catalogue.Decode([]byte(raw))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-72
View File
@@ -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())
}
}
}
}
@@ -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.
-12
View File
@@ -219,15 +219,3 @@ func theGraph(ctx context.Context, inv *inventory.Inventory) ([]inventory.Entry,
} }
return entries, read, edges, nil 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 "([^"]+)"`)
+20 -8
View File
@@ -51,6 +51,8 @@ var errKeyFailed = errors.New("the builder's key failed")
type keyReads interface { type keyReads interface {
Build(ctx context.Context, module, fp string) (builderkeys.Key, error) Build(ctx context.Context, module, fp string) (builderkeys.Key, error)
Manifest(ctx context.Context, digest string) (sdkdelivery.Manifest, 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 // 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 { if err != nil {
return waits("%s's manifest: %v", t.Module, err) 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 { 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 "+ 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 { if manifest.Module != t.Module {
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the key of %s names the manifest of %s", return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the key of %s names the manifest of %s",
+38 -7
View File
@@ -23,6 +23,14 @@ import (
type keyBuckets struct { type keyBuckets struct {
keys map[string]builderkeys.Key keys map[string]builderkeys.Key
manifests map[string]string 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) { func (k keyBuckets) Build(_ context.Context, module, fp string) (builderkeys.Key, error) {
@@ -148,15 +156,38 @@ 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. // ADR 0578 §3, §6 (changing ADR 0501 §7): the controller judges no manifest. A word it does not read is set aside and the
func TestAManifestWordTheControllerDoesNotReadWaits(t *testing.T) { // module registered with no second build; the module the indexer's verdict refuses is held and named, never failed.
raw := `{"module":"x","version":"1","not-a-word-yet":true,"build":{"artifacts":[{"name":"a","kind":"archive","from":"files"}]}, 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"}]}` "resources":[{"id":"a","type":"archive","path":"/opt/x","artifact":"a"}]}`
keys, trunk := aKeyOf(raw, 1, sdkdelivery.BuiltArtifact{Name: "a", Kind: "archive", Digest: shaOf("a"), 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, ""},
{"a fallback to a predefined key, no list at hand", `{"module":"x","version":"1","config":{"font-size":{"type":"number",
"description":"size","default":10,"predefined":"font-size"}},` + 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")}) 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{}) _, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{})
if !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), `"not-a-word-yet"`) { switch {
t.Fatalf("an unread word: %v", err) 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)
}
} }
} }
@@ -215,7 +246,7 @@ func TestATemplatedModulesTextsAreReadFromItsArchive(t *testing.T) {
// The launchers a key's bundle is resolved with are mesh-sdk's one rule (buildstep.Launchers), the names // The launchers a key's bundle is resolved with are mesh-sdk's one rule (buildstep.Launchers), the names
// mesh-build-node writes: a TypeScript bundle's entrypoint x.js runs as x.serve.mjs, a Go bundle has none. // mesh-build-node writes: a TypeScript bundle's entrypoint x.js runs as x.serve.mjs, a Go bundle has none.
func TestALaunchersRuleIsTheBuildModules(t *testing.T) { func TestALaunchersRuleIsTheBuildModules(t *testing.T) {
m, err := catalogue.ParseManifest([]byte(tsBundle)) m, err := catalogue.Decode([]byte(tsBundle))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -23,7 +23,7 @@ func leftOutNFS(t *testing.T) catalogue.Resolution {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m, err := catalogue.ParseManifest(raw) m, err := catalogue.Decode(raw)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -73,12 +73,10 @@ func buildFrom(result link.BuildResult) inventory.Build {
kept.Manifest = recordedManifest(result.Manifest, announced) kept.Manifest = recordedManifest(result.Manifest, announced)
// The module name comes from the manifest, which only exists when the build got that far. // The module name comes from the manifest, which only exists when the build got that far.
if len(kept.Manifest) > 0 { if len(kept.Manifest) > 0 {
if list, err := judgedWith(result); err == nil { if m, err := catalogue.Decode(kept.Manifest); err == nil {
if m, err := catalogue.ParseManifestWith(kept.Manifest, list); err == nil {
kept.Module = m.Module kept.Module = m.Module
} }
} }
}
return kept return kept
} }
@@ -130,15 +128,7 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
return catalogue.Manifest{}, kept, fmt.Errorf("%s could not build %s:\n%s", return catalogue.Manifest{}, kept, fmt.Errorf("%s could not build %s:\n%s",
result.On, result.Repository, result.Failed) result.On, result.Repository, result.Failed)
} }
// Judged first against the list the build was judged against: the catalogue's at the commit built (novox/hq manifest, err := catalogue.Decode(kept.Manifest)
// ADR 0348 §7), so a module naming a key its own commit adds is read; and again below against the list the
// mesh publishes, before it is registered.
list, err := judgedWith(result)
if err != nil {
return catalogue.Manifest{}, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", result.On,
result.Repository, short(result.Commit), err)
}
manifest, err := catalogue.ParseManifestWith(kept.Manifest, list)
if err != nil { if err != nil {
return catalogue.Manifest{}, kept, fmt.Errorf("%s built %s and what came back is not a manifest: %w", return catalogue.Manifest{}, kept, fmt.Errorf("%s built %s and what came back is not a manifest: %w",
result.On, result.Repository, err) result.On, result.Repository, err)
@@ -268,16 +258,6 @@ func carriedList(result link.BuildResult) (catalogue.PredefinedList, error) {
return list, nil return list, nil
} }
// judgedWith is the list a build's manifest is first judged against at registration: the catalogue's own at the
// commit built, which its build judged it against, where it carried one; the list kept otherwise.
func judgedWith(result link.BuildResult) (catalogue.PredefinedList, error) {
list, err := carriedList(result)
if err != nil || list != nil {
return list, err
}
return catalogue.PublishedPredefined(), nil
}
// publishPredefined keeps a trunk build's list when it is newer than the one kept, says so, and loads what the // publishPredefined keeps a trunk build's list when it is newer than the one kept, says so, and loads what the
// store keeps then — this build's, or a newer one another controller process kept. // store keeps then — this build's, or a newer one another controller process kept.
func publishPredefined(ctx context.Context, inv *inventory.Inventory, result link.BuildResult, func publishPredefined(ctx context.Context, inv *inventory.Inventory, result link.BuildResult,
-1
View File
@@ -223,7 +223,6 @@ var usageText = `mesh-controller — the control plane
overlay place <node> [flags] say where a node is and how it is reached overlay place <node> [flags] say where a node is and how it is reached
overlay show the private network, as the mesh computes it overlay show the private network, as the mesh computes it
module add <file> register a module from its manifest module add <file> register a module from its manifest
module check <file|dir>... judge manifests where they are written, with no mesh (exit 1 on any problem)
module list what modules this mesh knows about module list what modules this mesh knows about
module moved <name> <commit> the source has a newer commit than the mesh built module moved <name> <commit> the source has a newer commit than the mesh built
module forget <name> remove one, unless a node runs it or the mesh holds things for it module forget <name> remove one, unless a node runs it or the mesh holds things for it
+6 -60
View File
@@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"path/filepath"
"slices" "slices"
"sort" "sort"
"strings" "strings"
@@ -64,35 +63,7 @@ var provided = providedModules()
func moduleCommand(ctx context.Context, args []string) error { func moduleCommand(ctx context.Context, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("module add <file>, module check <file>..., module list, or module forget <name>") return errors.New("module add <file>, module list, or module forget <name>")
}
// `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)
} }
open, err := openStores(ctx) open, err := openStores(ctx)
if err != nil { if err != nil {
@@ -126,21 +97,19 @@ func moduleCommand(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
m, err := catalogue.ParseManifest(raw) m, err := catalogue.Decode(raw)
if err != nil { if err != nil {
return err return err
} }
// A manifest handed over by hand takes each template's text from the tree beside it (novox/hq ADR 0349 §2). // A manifest handed over by hand is not judged here (ADR 0578 §3): a templated one has no build to read its
if m, err = catalogue.WithTemplatesFrom(m, filepath.Dir(positionals[0])); err != nil { // texts from, so it is refused by name.
return err if p := catalogue.HeldTemplateProblem(m); p != "" {
return errors.New(p)
} }
from, err := whereItComesFrom(*repo, *ref, *commit, *path, *self) from, err := whereItComesFrom(*repo, *ref, *commit, *path, *self)
if err != nil { if err != nil {
return err return err
} }
if err := namesNoInstallation(m); err != nil {
return err
}
if err := inv.RegisterModule(ctx, m, from); err != nil { if err := inv.RegisterModule(ctx, m, from); err != nil {
return err return err
} }
@@ -1416,29 +1385,6 @@ func whereItComesFrom(repository, ref, commit, path string, self bool) (inventor
return from, nil return from, nil
} }
// namesNoInstallation is the mesh refusing a definition that names an installation, at the moment
// it would enter the catalogue (novox/hq ADR 0112, ADR 0155). `module check` says the same thing
// earlier, where the author is; this is the last moment the mesh can still say no, and a
// definition that got past the check — written elsewhere, or checked by nobody — is refused here
// in the same words. A name meant on purpose is declared with its reason and passes.
func namesNoInstallation(m catalogue.Manifest) error {
if problems := catalogue.TrustProblems(m); len(problems) > 0 {
return fmt.Errorf("%s", strings.Join(problems, "; "))
}
// And 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, never where a stored build is read again.
if problems := catalogue.WrittenSettingsProblems(m); len(problems) > 0 {
return fmt.Errorf("%s", strings.Join(problems, "; "))
}
named := catalogue.InstallationProblems(m)
if len(named) == 0 {
return nil
}
return fmt.Errorf("%s names an installation, and a definition names none — declare a name meant "+
"on purpose under %s with its reason, or take it out:\n - %s",
m.Module, catalogue.NamesOnPurpose, strings.Join(named, "\n - "))
}
// seatClaimed is one seat a module claims, as its runtime needs it: the name, the scope (a // seatClaimed is one seat a module claims, as its runtime needs it: the name, the scope (a
// node-scoped seat's verb carries the machine, design 33 §4) and the verbs the seat promises. // node-scoped seat's verb carries the machine, design 33 §4) and the verbs the seat promises.
type seatClaimed struct { type seatClaimed struct {
@@ -18,6 +18,7 @@ import (
// one module's directory and touches nothing else the mesh holds: nothing asks its first build, it opens no plan, and // one module's directory and touches nothing else the mesh holds: nothing asks its first build, it opens no plan, and
// the module is registered from the builder's first done key of it, assigned nowhere, with no build request written. // the module is registered from the builder's first done key of it, assigned nowhere, with no build request written.
func TestAMergeAddingAModuleEndsRegisteredFromItsKeyAndAsksNoBuild(t *testing.T) { func TestAMergeAddingAModuleEndsRegisteredFromItsKeyAndAsksNoBuild(t *testing.T) {
withNoNewModuleWaits(t)
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
inv := open.inventory inv := open.inventory
@@ -66,9 +67,11 @@ func TestAMergeAddingAModuleEndsRegisteredFromItsKeyAndAsksNoBuild(t *testing.T)
} }
} }
// ADR 0501 §7: a new module whose manifest uses a word this controller does not read is named waiting at once, never // ADR 0578 §6 (which changes ADR 0501 §7): a new module whose manifest uses a word this controller does not read is
// failed, and registered from the same key, with no second build, when a controller that reads it looks. // registered at once, the word set aside, never failed and with no second build; one whose manifest the indexer's judge
func TestANewModuleWithAnUnreadWordWaitsForTheControllerThatReadsIt(t *testing.T) { // refused is held, named in the indexer's words, and never registered (ADR 0578 §3).
func TestANewModuleWithAnUnreadWordIsRegisteredAndARefusedOneIsHeld(t *testing.T) {
withNoNewModuleWaits(t)
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
inv := open.inventory inv := open.inventory
@@ -94,12 +97,38 @@ func TestANewModuleWithAnUnreadWordWaitsForTheControllerThatReadsIt(t *testing.T
} }
b.built(t, "watchman", "1", fp, digest, sdkdelivery.BuildDone) b.built(t, "watchman", "1", fp, digest, sdkdelivery.BuildDone)
takeNewModules(ctx, open, "watchman") takeNewModules(ctx, open, "watchman")
if _, known := inventoryHolds(t, inv, "watchman"); known { if _, known := inventoryHolds(t, inv, "watchman"); !known {
t.Fatal("0501 §7: a manifest with an unread word was registered") t.Fatalf("0578 §6: a manifest with a word set aside is not registered: %+v", newModuleObservations(time.Now()))
} }
obs := newModuleObservations(time.Now()) // The indexer refused another: held, named, not registered.
if len(obs) != 1 || obs[0].Kind != kindNewModuleWaits || !strings.Contains(obs[0].Summary, `"not-a-word-yet"`) { refusedFP := fpOf('9')
t.Fatalf("0501 §7: the module waiting for a reader of its word is named by %+v", obs) refusedRaw := `{"module":"refusedmod","version":"1","build":{"artifacts":[{"name":"a","kind":"archive","from":"files"}]},` +
`"resources":[{"id":"a","type":"archive","path":"/opt/refusedmod","artifact":"a"}]}`
refusedDigest := sdkdelivery.ManifestDigest([]byte(refusedRaw))
value, _ = sdkdelivery.EncodeManifest(sdkdelivery.Manifest{Module: "refusedmod", JSON: refusedRaw})
if _, err := b.manifests.Put(ctx, refusedDigest, value); err != nil {
t.Fatal(err)
}
verdict, _ := sdkdelivery.EncodeManifestVerdict(sdkdelivery.ManifestVerdict{Module: "refusedmod", State: sdkdelivery.VerdictRefused,
Problems: []string{"it breaks a rule"}, Judge: "exe:1", At: time.Now()})
if _, err := b.verdicts.Put(ctx, refusedDigest, verdict); err != nil {
t.Fatal(err)
}
trunk, _ = sdkdelivery.EncodeModule(sdkdelivery.Module{FP: "src1:" + refusedFP[5:], FP2: refusedFP, Manifest: refusedDigest,
Repository: "novox/mesh-catalog", Commit: "5555555500000000", Path: "modules/refusedmod"})
if _, err := b.modules.Put(ctx, "refusedmod", trunk); err != nil {
t.Fatal(err)
}
b.built(t, "refusedmod", "1", refusedFP, refusedDigest, sdkdelivery.BuildDone)
takeNewModules(ctx, open, "refusedmod")
if _, known := inventoryHolds(t, inv, "refusedmod"); known {
t.Fatal("0578 §3: a manifest the indexer refused was registered")
}
// Said at once in the controller's journal; a condition once it waits past the bound (the watchman raises the
// indexer's manifest-refused at once).
obs := newModuleObservations(time.Now().Add(newModuleWaitBound + time.Minute))
if len(obs) != 1 || obs[0].Kind != kindNewModuleWaits || !strings.Contains(obs[0].Summary, "refused the manifest of refusedmod: it breaks a rule") {
t.Fatalf("0578 §3: the refused module is not held and named: %+v", obs)
} }
builds, _ := inv.Builds(ctx, "watchman", 5) builds, _ := inv.Builds(ctx, "watchman", 5)
for _, b := range builds { for _, b := range builds {
+13 -5
View File
@@ -9,11 +9,7 @@ import (
// ADR 0501 §6, §7 (issue 1851, A5b): the condition of a new module reads its key's take-in: one waiting for a controller // ADR 0501 §6, §7 (issue 1851, A5b): the condition of a new module reads its key's take-in: one waiting for a controller
// that reads its manifest's word is named at once, one waiting for its key only past the bound, each reason said once. // that reads its manifest's word is named at once, one waiting for its key only past the bound, each reason said once.
func TestANewModuleWaitingIsNamedByWhatItWaitsFor(t *testing.T) { func TestANewModuleWaitingIsNamedByWhatItWaitsFor(t *testing.T) {
t.Cleanup(func() { withNoNewModuleWaits(t)
newModulesWaiting.Lock()
newModulesWaiting.by = map[string]newModuleWait{}
newModulesWaiting.Unlock()
})
word := `waits: watchman's manifest uses the word "key-ttl", which this controller does not read: it is registered when a controller that reads it runs` word := `waits: watchman's manifest uses the word "key-ttl", which this controller does not read: it is registered when a controller that reads it runs`
sayNewModuleWait(newModuleWait{Module: "watchman", Commit: "c1", Why: word, Word: wordIn(word)}) sayNewModuleWait(newModuleWait{Module: "watchman", Commit: "c1", Why: word, Word: wordIn(word)})
sayNewModuleWait(newModuleWait{Module: "view", Commit: "c1", Why: "waits: the build of view at aaaa is building on ace"}) sayNewModuleWait(newModuleWait{Module: "view", Commit: "c1", Why: "waits: the build of view at aaaa is building on ace"})
@@ -31,3 +27,15 @@ func TestANewModuleWaitingIsNamedByWhatItWaitsFor(t *testing.T) {
t.Fatalf("registered modules are still named: %+v", obs) t.Fatalf("registered modules are still named: %+v", obs)
} }
} }
// withNoNewModuleWaits empties the controller's list of new modules waiting, for a test, at its start and at its end: the
// list is the process's, so a test that leaves a module in it is read by the next.
func withNoNewModuleWaits(t *testing.T) {
reset := func() {
newModulesWaiting.Lock()
newModulesWaiting.by = map[string]newModuleWait{}
newModulesWaiting.Unlock()
}
reset()
t.Cleanup(reset)
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// paths and its manifest alone. mesh-sdk's TypeScript package at its repository's root: a change to its go/ touches no // paths and its manifest alone. mesh-sdk's TypeScript package at its repository's root: a change to its go/ touches no
// module, one to its src/ or its package.json touches it. A module with an artifact naming no paths reads its directory. // module, one to its src/ or its package.json touches it. A module with an artifact naming no paths reads its directory.
func TestAModuleNamingItsPathsIsReadFromThemAlone(t *testing.T) { func TestAModuleNamingItsPathsIsReadFromThemAlone(t *testing.T) {
m, err := catalogue.ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"lib", m, err := catalogue.Decode([]byte(`{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"lib",
"kind":"package","language":"typescript","paths":["src","package.json","package-lock.json","tsconfig.json"]}]}, "kind":"package","language":"typescript","paths":["src","package.json","package-lock.json","tsconfig.json"]}]},
"resources":[]}`)) "resources":[]}`))
if err != nil { if err != nil {
@@ -21,7 +21,7 @@ func aMeshWithTheWordsInItsLayers(t *testing.T) (*stores, wordedMesh) {
register(t, open, catalogue.Manifest{Module: "proxy", Version: "1", Provides: catalogue.FromAnywhere("route"), register(t, open, catalogue.Manifest{Module: "proxy", Version: "1", Provides: catalogue.FromAnywhere("route"),
Receives: map[string]string{"route": "/var/lib/proxy/routes.json"}}) Receives: map[string]string{"route": "/var/lib/proxy/routes.json"}})
for _, text := range w.Modules { for _, text := range w.Modules {
m, err := catalogue.ParseManifest([]byte(strings.ReplaceAll(string(text), "IMAGE", image))) m, err := catalogue.Decode([]byte(strings.ReplaceAll(string(text), "IMAGE", image)))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -10,7 +10,10 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
) )
func TestRegistrationPublishesTheNewestTrunkListAndJudgesEveryModuleAgainstIt(t *testing.T) { // Registration publishes the newest trunk's list of predefined keys. It judges no manifest against it (ADR 0578 §3): the
// indexer's judge does, with the list it keeps, and a fallback to a key not on it, or of another type, is refused there
// (mesh-delivery-indexer judge's tests).
func TestRegistrationPublishesTheNewestTrunkList(t *testing.T) {
withoutAPublishedList(t) withoutAPublishedList(t)
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
@@ -28,22 +31,6 @@ func TestRegistrationPublishesTheNewestTrunkListAndJudgesEveryModuleAgainstIt(t
t.Fatalf("the controller judges against %v, not the list published", got) t.Fatalf("the controller judges against %v, not the list published", got)
} }
// One naming a key not on the list, or of another type, is refused and never enters the catalogue.
for i, c := range []struct{ manifest, says string }{
{notifierFallingBackTo("font-scale", "number"), "not on the list"},
{notifierFallingBackTo("font-size", "string"), "which is a number"},
} {
id := []string{"b-2", "b-3"}[i]
if _, _, err := takeIn(ctx, open.inventory, catalogueBuild(id, c.manifest, theFirstKeys, 10)); err == nil ||
!strings.Contains(err.Error(), c.says) {
t.Errorf("wanted the registration refused saying %q, got %v", c.says, err)
}
}
src, err := open.inventory.SourceOf(ctx, "notifier")
if err != nil || src.BuiltFrom != strings.Repeat("1", 12) {
t.Fatalf("a refused build replaced the registered one: %+v %v", src, err)
}
// A newer build's list, which drops agent-managed-settings, replaces the published one. // A newer build's list, which drops agent-managed-settings, replaces the published one.
smaller := `{"font-size": {"type": "number", "description": "points", "minimum": 6, "maximum": 72}}` smaller := `{"font-size": {"type": "number", "description": "points", "minimum": 6, "maximum": 72}}`
if _, _, err := takeIn(ctx, open.inventory, catalogueBuild("b-4", if _, _, err := takeIn(ctx, open.inventory, catalogueBuild("b-4",
@@ -60,7 +47,7 @@ func TestRegistrationPublishesTheNewestTrunkListAndJudgesEveryModuleAgainstIt(t
t.Fatalf("an older build's list heard later replaced the newer one: %+v", kept) t.Fatalf("an older build's list heard later replaced the newer one: %+v", kept)
} }
// A build of another repository publishes nothing, and its manifest is judged against the published list. // A build of another repository publishes nothing.
other := catalogueBuild("b-6", notifierFallingBackTo("font-size", "number"), theFirstKeys, 99) other := catalogueBuild("b-6", notifierFallingBackTo("font-size", "number"), theFirstKeys, 99)
other.Repository, other.Source.Repository = "http://forge.internal:20000/novox/notifier.git", "novox/notifier" other.Repository, other.Source.Repository = "http://forge.internal:20000/novox/notifier.git", "novox/notifier"
other.Manifest = json.RawMessage(strings.Replace(notifierFallingBackTo("font-size", "number"), `"notifier"`, other.Manifest = json.RawMessage(strings.Replace(notifierFallingBackTo("font-size", "number"), `"notifier"`,
@@ -71,9 +58,4 @@ func TestRegistrationPublishesTheNewestTrunkListAndJudgesEveryModuleAgainstIt(t
if kept, _, _ = open.inventory.PredefinedKept(ctx); kept.Build != "b-4" { if kept, _, _ = open.inventory.PredefinedKept(ctx); kept.Build != "b-4" {
t.Fatalf("a build of another repository published its list: %+v", kept) t.Fatalf("a build of another repository published its list: %+v", kept)
} }
other.ID, other.Manifest = "b-7", json.RawMessage(strings.Replace(notifierFallingBackTo("agent-managed-settings",
"number"), `"notifier"`, `"chimes"`, 1))
if _, _, err := takeIn(ctx, open.inventory, other); err == nil || !strings.Contains(err.Error(), "not on the list") {
t.Fatalf("a key only another repository's list holds was registered: %v", err)
}
} }
+2 -59
View File
@@ -1,10 +1,7 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@@ -12,8 +9,8 @@ import (
"github.com/novox/mesh-controller/internal/link" "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 // The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661): the
// merge gate judge each manifest against the list at the root of its tree; registration publishes the newest // 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. // trunk build's list of the catalogue and judges every module against the list the mesh publishes.
const theFirstKeys = `{ const theFirstKeys = `{
@@ -41,60 +38,6 @@ func withoutAPublishedList(t *testing.T) {
t.Cleanup(func() { catalogue.ReplacePublished(was) }) 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. // catalogueBuild is a trunk build of the catalogue carrying the list at a generation.
func catalogueBuild(id, manifest, list string, generation int) link.BuildResult { func catalogueBuild(id, manifest, list string, generation int) link.BuildResult {
r := link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git", r := link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git",
@@ -20,7 +20,7 @@ import (
func gainedAndPlain(t *testing.T) (plain, gained catalogue.Manifest) { func gainedAndPlain(t *testing.T) (plain, gained catalogue.Manifest) {
t.Helper() t.Helper()
plain, err := catalogue.ParseManifest([]byte(`{"module":"mesh-mail","version":"1", plain, err := catalogue.Decode([]byte(`{"module":"mesh-mail","version":"1",
"own-secrets":{"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}}, "own-secrets":{"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}},
"resources":[{"id":"account","type":"user","name":"mesh-mail-send"}]}`)) "resources":[{"id":"account","type":"user","name":"mesh-mail-send"}]}`))
if err != nil { if err != nil {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// declares bus, whose own secret broker-send is where module issue delivers its account (ADR 0436 §2), // declares bus, whose own secret broker-send is where module issue delivers its account (ADR 0436 §2),
// owned by mesh-mail-send, the user resource the process declares, at mode 0600 as every own secret is. // owned by mesh-mail-send, the user resource the process declares, at mode 0600 as every own secret is.
func mailSenderWithProcess() catalogue.Manifest { func mailSenderWithProcess() catalogue.Manifest {
m, err := catalogue.ParseManifest([]byte(`{"module":"mesh-mail","version":"1","emits":["mail.send.sent"], m, err := catalogue.Decode([]byte(`{"module":"mesh-mail","version":"1","emits":["mail.send.sent"],
"own-secrets":{"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}}, "own-secrets":{"broker-send":{"path":"/var/lib/mesh-mail/broker-send","owner":"mesh-mail-send"}},
"resources":[{"id":"account","type":"user","name":"mesh-mail-send"}, "resources":[{"id":"account","type":"user","name":"mesh-mail-send"},
{"id":"send","type":"process","run":["./mesh-mail","send"],"user":"mesh-mail-send", {"id":"send","type":"process","run":["./mesh-mail","send"],"user":"mesh-mail-send",
+1 -1
View File
@@ -23,7 +23,7 @@ import (
// invoked seat verb), so the account module issue delivers for it can be put through every grant at once. // invoked seat verb), so the account module issue delivers for it can be put through every grant at once.
func mailSenderLiveManifest(t *testing.T) catalogue.Manifest { func mailSenderLiveManifest(t *testing.T) catalogue.Manifest {
t.Helper() t.Helper()
m, err := catalogue.ParseManifest([]byte(`{"module":"mesh-mail","version":"1", m, err := catalogue.Decode([]byte(`{"module":"mesh-mail","version":"1",
"emits":["mail.send.sent","mail.send.failed"],"state":["sends"], "emits":["mail.send.sent","mail.send.failed"],"state":["sends"],
"streams":[{"name":"mail-sends","events":["mail.send.approved"],"max-age-days":1,"max-mib":8, "streams":[{"name":"mail-sends","events":["mail.send.approved"],"max-age-days":1,"max-mib":8,
"retention":"work-queue","max-deliveries":5}], "retention":"work-queue","max-deliveries":5}],
+1 -1
View File
@@ -33,7 +33,7 @@ func TestReplay239AModuleNameIsNotTakenOverByAnotherRepository(t *testing.T) {
register := func(raw, repository, path string) { register := func(raw, repository, path string) {
t.Helper() t.Helper()
m, err := catalogue.ParseManifest([]byte(raw)) m, err := catalogue.Decode([]byte(raw))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -14,7 +14,7 @@ import (
func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) { func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) {
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
forge, err := catalogue.ParseManifest([]byte(`{"module":"forge","version":"1","requires":["route"], forge, err := catalogue.Decode([]byte(`{"module":"forge","version":"1","requires":["route"],
"provides":[{"name":"npm-package-registry","scope":"mesh"}], "provides":[{"name":"npm-package-registry","scope":"mesh"}],
"contributes":{"route":{"web":{"label":"git","port":3000}}}, "contributes":{"route":{"web":{"label":"git","port":3000}}},
"serves":{"npm-package-registry":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/", "serves":{"npm-package-registry":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/",
+1 -28
View File
@@ -1,14 +1,10 @@
package main package main
import ( import (
"bytes"
"crypto/ecdh" "crypto/ecdh"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"os"
"path/filepath"
"strings"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
@@ -41,7 +37,7 @@ func TestTheProvidersMachineReceivesTheSharesOfAnS3BucketRequirement(t *testing.
if err := json.Unmarshal(raw, &overlayModule); err != nil { if err := json.Unmarshal(raw, &overlayModule); err != nil {
t.Fatal(err) t.Fatal(err)
} }
consumer, err := catalogue.ParseManifest([]byte(filesRequiringShares)) consumer, err := catalogue.Decode([]byte(filesRequiringShares))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -79,26 +75,3 @@ func TestTheProvidersMachineReceivesTheSharesOfAnS3BucketRequirement(t *testing.
t.Fatalf("the provider's grant for the consumer carries shares %s, want %s", got, want) 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())
}
}
+1 -1
View File
@@ -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, 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) 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 // Repairs done by hand this week (novox/hq to-be 45 §7). Not a fault, so it does not break "all
-123
View File
@@ -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.
+3 -2
View File
@@ -20,11 +20,12 @@ const (
) )
// unknownFieldObservations is one condition for each module of the catalogue whose stored manifest has // 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 { func unknownFieldObservations(known map[string]catalogue.Manifest) []conditions.Observation {
names := make([]string, 0, len(known)) names := make([]string, 0, len(known))
for name, m := range known { for name, m := range known {
if m.UnknownField() != "" { if m.ReadNoFurther() {
names = append(names, name) names = append(names, name)
} }
} }
+7 -3
View File
@@ -11,14 +11,18 @@ import (
// A module whose stored manifest has a key this controller does not know is a condition, in plain // 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). // words, until it is read again; every other module raises nothing (novox/hq ADR 0262).
func TestAModuleWithAnUnknownFieldIsACondition(t *testing.T) { func TestAModuleWithAnUnknownFieldIsACondition(t *testing.T) {
var later, now catalogue.Manifest var later, now, aside catalogue.Manifest
if err := json.Unmarshal([]byte(`{"module": "dunst", "version": "2", "settings": {}, "a-field-from-later": 1}`), &later); err != nil { 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) t.Fatal(err)
} }
if err := json.Unmarshal([]byte(`{"module": "xorg", "version": "1"}`), &now); err != nil { if err := json.Unmarshal([]byte(`{"module": "xorg", "version": "1"}`), &now); err != nil {
t.Fatal(err) 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 { if len(observed) != 1 || observed[0].ID != "dunst" || observed[0].Kind != kindUnknownField {
t.Fatalf("observed: %+v", observed) t.Fatalf("observed: %+v", observed)
} }
@@ -1,46 +0,0 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"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.
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"}},
"resources":[
{"id":"user-js","type":"file","path":"/var/lib/mail-client/user.js","mode":"0644","content":"${setting:prefs}\n"},
{"id":"accounts","type":"file","path":"/var/lib/mail-client/accounts.json","mode":"0600","merge":"json",
"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)
if err != nil {
t.Fatalf("reading it is not where it is judged: %v", err)
}
if err := namesNoInstallation(m); err == nil || !strings.Contains(err.Error(), "names no takes-settings") {
t.Fatalf("registration passed it: %v", err)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
go 1.26.0 go 1.26.0
require ( 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.20261101022206-8af6e7266915
github.com/jackc/pgx/v5 v5.10.0 github.com/jackc/pgx/v5 v5.10.0
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,7 +1,7 @@
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYgA9TCv+4K8JgaURg3zQppvUgJLaDMP4= git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYgA9TCv+4K8JgaURg3zQppvUgJLaDMP4=
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE= 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.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
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.20261101022206-8af6e7266915/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 h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= 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= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+1 -52
View File
@@ -105,11 +105,7 @@ func theCataloguesEvents(t *testing.T) ([]AnEmitter, []AConsumer, []DeclaredSeat
emitters = append(emitters, *emitter) emitters = append(emitters, *emitter)
} }
if consumer != nil { if consumer != nil {
kept, setAside := setAsideRetiredSeats(*consumer) consumers = append(consumers, *consumer)
for _, note := range setAside {
t.Log(note)
}
consumers = append(consumers, kept)
} }
seats = append(seats, declared...) seats = append(seats, declared...)
} }
@@ -119,53 +115,6 @@ func theCataloguesEvents(t *testing.T) ([]AnEmitter, []AConsumer, []DeclaredSeat
return emitters, consumers, seats return emitters, consumers, seats
} }
// setAsideRetiredSeats takes out of a consumer the events of a seat the mesh has retired, and says each one.
// The check judges against the catalogue's trunk (ADR 0484 clause 4), and a group that retires a seat while
// its catalogue pull request drops the consume (ADR 0524 section 5) meets the trunk still holding it: against
// that catalogue the seat is gone and the subscription falls back to a module subject nobody owns. It is set
// aside and logged, never silently dropped; an unknown seat that is not retired is still judged.
func setAsideRetiredSeats(c AConsumer) (AConsumer, []string) {
kept := AConsumer{Module: c.Module}
var notes []string
for _, want := range c.Consumes {
seat, event, named := strings.Cut(want, ".")
if replacement, retired := catalogue.RetiredSeats[seat]; named && retired {
notes = append(notes, c.Module+" still consumes "+seat+"."+event+"; the seat is retired (its work is "+
replacement+"); the catalogue drops it in the same change (ADR 0524 §5), and the check judges against the catalogue's trunk (ADR 0484 clause 4)")
continue
}
kept.Consumes = append(kept.Consumes, want)
}
return kept, notes
}
func TestARetiredSeatsConsumeIsSetAsideAndAnUnknownOneIsNot(t *testing.T) {
var retired string
for name := range catalogue.RetiredSeats {
retired = name
break
}
if retired == "" {
t.Skip("no seat is retired")
}
kept, notes := setAsideRetiredSeats(AConsumer{Module: "m",
Consumes: []string{retired + ".built", "no-such-seat.built", "plain"}})
if len(notes) != 1 || !strings.Contains(notes[0], retired+".built") {
t.Fatalf("the retired seat's consume was not set aside and said: %v", notes)
}
if strings.Join(kept.Consumes, ",") != "no-such-seat.built,plain" {
t.Fatalf("what stays to be judged is wrong: %v", kept.Consumes)
}
if bad := Disagreements([]AnEmitter{{Module: "other", Emits: []string{"x"}}},
[]AConsumer{kept}, nil); len(bad) != 0 {
t.Fatalf("unexpected: %v", bad)
}
if bad := Disagreements([]AnEmitter{{Module: "no-such-seat", Emits: []string{"x"}}},
[]AConsumer{kept}, nil); len(bad) != 1 {
t.Fatalf("an unknown, non-retired consume no longer fails: %v", bad)
}
}
// **Do the derived subjects meet, not just the names?** // **Do the derived subjects meet, not just the names?**
// //
// The check above compares what a consumer asks for against what an emitter says it emits, by name. It // The check above compares what a consumer asks for against what an emitter says it emits, by name. It
+1 -1
View File
@@ -108,7 +108,7 @@ func (s ModuleStream) Stream() Stream {
// QueueOwner is the one process of a module that is the durable consumer of one of its own work-queue // QueueOwner is the one process of a module that is the durable consumer of one of its own work-queue
// streams (novox/hq ADR 0436 §1, §2, §6): named in that process's bus.queues, and checked at registration // streams (novox/hq ADR 0436 §1, §2, §6): named in that process's bus.queues, and checked at registration
// to be the only one (catalogue.WorkQueueStreamProblems). // to be the only one (the indexer's judge).
type QueueOwner struct { type QueueOwner struct {
// Process is the process resource's id — its bus account's name, with the node and module (novox/hq ADR // Process is the process resource's id — its bus account's name, with the node and module (novox/hq ADR
// 0436 §2). // 0436 §2).
+25
View File
@@ -255,6 +255,31 @@ func (r Reader) Manifest(ctx context.Context, digest string) (delivery.Manifest,
return m, nil 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 // 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 // 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. // it is: the watch looks again each time wake would have been called for the other bucket, and at every retry.
+1 -1
View File
@@ -60,7 +60,7 @@ func TestTheAsksLabBus(t *testing.T) {
} }
var manifests []catalogue.Manifest var manifests []catalogue.Manifest
read := func(raw []byte, from string) { read := func(raw []byte, from string) {
m, err := catalogue.ParseManifest(raw) m, err := catalogue.Decode(raw)
if err != nil { if err != nil {
t.Fatalf("%s: %v", from, err) t.Fatalf("%s: %v", from, err)
} }
+1 -1
View File
@@ -476,7 +476,7 @@ func streamsOf(m catalogue.Manifest) []broker.ModuleStream {
// queueOwnersOf is every work-queue stream of a module's own that one of its processes names in bus.queues, // queueOwnersOf is every work-queue stream of a module's own that one of its processes names in bus.queues,
// with that process's id and the stream's declared max-deliveries (novox/hq ADR 0436 §1, §2, §6). In // with that process's id and the stream's declared max-deliveries (novox/hq ADR 0436 §1, §2, §6). In
// resource order, so composition is repeatable; a queue naming a stream the module does not own, or one two // resource order, so composition is repeatable; a queue naming a stream the module does not own, or one two
// processes name, is caught at registration (catalogue.WorkQueueStreamProblems) and never reaches here. // processes name, is caught at registration (the indexer's judge) and never reaches here.
func queueOwnersOf(m catalogue.Manifest) []broker.QueueOwner { func queueOwnersOf(m catalogue.Manifest) []broker.QueueOwner {
buses := m.ProcessBuses() buses := m.ProcessBuses()
if len(buses) == 0 { if len(buses) == 0 {
+1 -1
View File
@@ -15,7 +15,7 @@ import (
// configured is a module in the config form: one value a mergeable file takes, and a directory to place. // configured is a module in the config form: one value a mergeable file takes, and a directory to place.
func configured(t *testing.T, name string) catalogue.Manifest { func configured(t *testing.T, name string) catalogue.Manifest {
t.Helper() t.Helper()
m, err := catalogue.ParseManifest([]byte(`{"module": "` + name + `", "version": "1", m, err := catalogue.Decode([]byte(`{"module": "` + name + `", "version": "1",
"config": {"level": {"type": "string", "description": "how much it says", "default": "info"}}, "config": {"level": {"type": "string", "description": "how much it says", "default": "info"}},
"resources": [{"id": "data", "type": "directory", "mode": "0750"}, {"id": "conf", "type": "file", "path": "/etc/` + name + `.json", "mode": "0640", "resources": [{"id": "data", "type": "directory", "mode": "0750"}, {"id": "conf", "type": "file", "path": "/etc/` + name + `.json", "mode": "0640",
"content": "{\"level\": \"info\"}", "merge": "json", "takes-config": ["level"]}]}`)) "content": "{\"level\": \"info\"}", "merge": "json", "takes-config": ["level"]}]}`))
+2 -2
View File
@@ -44,12 +44,12 @@ func TestOnlyTheServingControllerMayAnswerRootFree(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
controller, err := catalogue.ParseManifest(raw) controller, err := catalogue.Decode(raw)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
parse := func(s string) catalogue.Manifest { parse := func(s string) catalogue.Manifest {
m, err := catalogue.ParseManifest([]byte(s)) m, err := catalogue.Decode([]byte(s))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -47,13 +47,10 @@ func TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed(t *testing.T
if err != nil || len(raw) == 0 { if err != nil || len(raw) == 0 {
t.Fatalf("the stored build is not read again: %d bytes, %v", len(raw), err) t.Fatalf("the stored build is not read again: %d bytes, %v", len(raw), err)
} }
m, err := catalogue.ParseManifest(raw) m, err := catalogue.Decode(raw)
if err != nil { if err != nil {
t.Fatalf("the stored manifest is not read again: %v", err) t.Fatalf("the stored manifest is not read again: %v", err)
} }
if written := catalogue.WrittenSettingsProblems(m); len(written) != 1 {
t.Fatalf("the rule this build predates should name its accounts file where it is written: %v", written)
}
// And composed, in today's form: the accounts file still takes its layer's keys. // And composed, in today's form: the accounts file still takes its layer's keys.
with := catalogue.Rendering{Settings: catalogue.SettingsBy{"mail-client": {{From: "workstation", with := catalogue.Rendering{Settings: catalogue.SettingsBy{"mail-client": {{From: "workstation",
+1 -1
View File
@@ -22,7 +22,7 @@ var adr0367Families = []string{`"mesh-knowledge.reviewed"`, `"mesh-delivery.tran
func composeWaits(t *testing.T, families []string, people map[string][]string) (granted, issued []string) { func composeWaits(t *testing.T, families []string, people map[string][]string) (granted, issued []string) {
t.Helper() t.Helper()
parse := func(s string) catalogue.Manifest { parse := func(s string) catalogue.Manifest {
m, err := catalogue.ParseManifest([]byte(s)) m, err := catalogue.Decode([]byte(s))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -18,7 +18,7 @@ func TestTheControllerIsAProcessAndNoContainer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m, err := ParseManifest(raw) m, err := Decode(raw)
if err != nil { if err != nil {
t.Fatalf("the controller's own manifest does not parse:\n%v", err) t.Fatalf("the controller's own manifest does not parse:\n%v", err)
} }
+2 -2
View File
@@ -18,7 +18,7 @@ func TestTheControllersOwnManifestClaimsItsSeat(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("the controller's own manifest is unreadable: %v", err) t.Fatalf("the controller's own manifest is unreadable: %v", err)
} }
m, err := ParseManifest(raw) m, err := Decode(raw)
if err != nil { if err != nil {
t.Fatalf("the controller's own manifest does not parse: %v", err) t.Fatalf("the controller's own manifest does not parse: %v", err)
} }
@@ -41,7 +41,7 @@ func TestTheControlPlanesOwnAddressesFollowTheNodesPorts(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m, err := ParseManifest(raw) m, err := Decode(raw)
if err != nil { if err != nil {
t.Fatalf("the control plane's own manifest does not parse:\n%v", err) t.Fatalf("the control plane's own manifest does not parse:\n%v", err)
} }
+1 -1
View File
@@ -34,7 +34,7 @@ func TestEveryManifestInUseClaimsASeatTheMeshDefines(t *testing.T) {
if raw, err = os.ReadFile(path); err != nil { if raw, err = os.ReadFile(path); err != nil {
t.Fatal(err) t.Fatal(err)
} }
m, err = catalogue.ParseManifest(raw) m, err = catalogue.Decode(raw)
} else { } else {
m, err = catalogue.ReadTreeManifest(root, filepath.Dir(path)) m, err = catalogue.ReadTreeManifest(root, filepath.Dir(path))
} }
+1 -1
View File
@@ -3,7 +3,7 @@ module route-proxy
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031122137-bb01a656d024 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
) )
+2 -2
View File
@@ -1,7 +1,7 @@
git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0 h1:89jE1oBK+l7gFJjz6wx8mUD3KxrqSDa7pUTlQkzC1do= git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0 h1:89jE1oBK+l7gFJjz6wx8mUD3KxrqSDa7pUTlQkzC1do=
git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0/go.mod h1:Yr+Xp89kpMiKH6kA06EU+d9I2CyzrK9yMX7J3pSv0hE= git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0/go.mod h1:Yr+Xp89kpMiKH6kA06EU+d9I2CyzrK9yMX7J3pSv0hE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031122137-bb01a656d024 h1:Uhg2DGxqh9+44ccSbF87v7joSodSwUEGUhhqtrGbP+k= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031122137-bb01a656d024/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/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 h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+3
View File
@@ -9,6 +9,9 @@
{ {
"name": "route", "name": "route",
"scope": "mesh", "scope": "mesh",
"credential": {
"form": "none"
},
"identity": false, "identity": false,
"answered-by": "helper" "answered-by": "helper"
} }
+1 -1
View File
@@ -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-catalog f3a57dcc56fb6c198095cafd11d0decff9f720c4 modules/*/module.json, modules/*/files, modules/*/templates, predefined.json
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
mesh-sdk 2da0de75c3f0087cff9c2f5ef789ca6dc3c59045 conformance/events/module-event.json mesh-sdk 8af6e72669151bf9b4f7524d38c28e29215514c6 conformance/events/module-event.json
To move them, from this repository's root, with the two repositories checked out beside it: To move them, from this repository's root, with the two repositories checked out beside it: