Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
542ce76c0a | ||
|
|
2882b5fcb1 | ||
|
|
481b1a7b05 |
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func moduleCheck(paths []string, 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{}
|
||||
failed := 0
|
||||
for _, path := range paths {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(out, "%s: %v\n", path, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
m, err := catalogue.ParseManifest(raw)
|
||||
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
|
||||
}
|
||||
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.
|
||||
problems := catalogue.CatalogueProblems(shelf)
|
||||
sort.Strings(problems)
|
||||
for _, p := range problems {
|
||||
fmt.Fprintln(out, p)
|
||||
}
|
||||
failed += len(problems)
|
||||
|
||||
var names []string
|
||||
for name := range shelf {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
m := shelf[name]
|
||||
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))
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
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\n", len(paths))
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func TestModuleCheckPassesTheCatalogue(t *testing.T) {
|
||||
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
||||
if _, err := os.Stat(root); err != nil {
|
||||
t.Skipf("catalogue sibling not present: %v", err)
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,7 @@ func usage() {
|
||||
overlay place <node> [flags] say where a node is and how it is reached
|
||||
overlay show the private network, as the mesh computes it
|
||||
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 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
|
||||
|
||||
@@ -54,7 +54,24 @@ var provided = providedModules()
|
||||
|
||||
func moduleCommand(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("module add <file>, module list, or module forget <name>")
|
||||
return errors.New("module add <file>, module check <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" {
|
||||
var paths []string
|
||||
for _, a := range args[1:] {
|
||||
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 moduleCheck(paths, os.Stdout)
|
||||
}
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
@@ -275,7 +292,7 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
return issueOnTheNewBus(ctx, inv, m, *forNode, busAddress)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("module has no %q; it has add, list, moved, forget and issue", args[0])
|
||||
return fmt.Errorf("module has no %q; it has add, check, list, moved, forget and issue", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A module that says it calls a tool may publish exactly that subject (novox/hq ADR 0152): the same
|
||||
// grant a person gets, derived the same way, so one list answers "what may this ask" for everybody.
|
||||
func TestAModuleMayAskOnlyTheToolsItInvokes(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "desk", Module: "mesh-console",
|
||||
Invokes: []string{"shop.price"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, perms.Publish, "mesh.mod.shop.tool.price")
|
||||
hasNot(t, perms.Publish, "mesh.mod.shop.tool.refund")
|
||||
hasNot(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||
}
|
||||
|
||||
// The console's grant: every tool, as one subject, and it reads as one.
|
||||
func TestAModuleInvokingEverythingMayAskAnyTool(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "desk", Module: "mesh-console",
|
||||
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||
}
|
||||
|
||||
// **A grant to call widens nothing else.** A module that invokes may not publish an event it did not
|
||||
// declare, may not answer as another module, and subscribes nothing it did not consume — the
|
||||
// difference between the console and a person is that the console is on a machine, not that it may
|
||||
// do more.
|
||||
func TestInvokingGrantsNothingButTheCall(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "desk", Module: "mesh-console",
|
||||
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range perms.Publish {
|
||||
if strings.Contains(p, ".event.") {
|
||||
t.Errorf("a module that only invokes may publish %q, an event it never declared", p)
|
||||
}
|
||||
if strings.HasPrefix(p, "mesh.seat.") {
|
||||
t.Errorf("a module that only invokes may publish %q, a seat it neither holds nor uses", p)
|
||||
}
|
||||
}
|
||||
for _, s := range perms.Subscribe {
|
||||
if strings.Contains(s, ".tool.") && !strings.HasPrefix(s, "mesh.mod.mesh-console.") {
|
||||
t.Errorf("a module that invokes may subscribe %q, another module's tools", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A module that declares no invokes calls nothing, which is every module but the console.
|
||||
func TestAModuleThatInvokesNothingCallsNothing(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "shop",
|
||||
Emits: []string{"order.placed"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range perms.Publish {
|
||||
if strings.Contains(p, ".tool.") {
|
||||
t.Errorf("a module with no invokes may publish %q", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The malformed entry is refused for a module as it is for a person, and in the same words.
|
||||
func TestAModulesToolGrantThatNamesNoToolIsRefused(t *testing.T) {
|
||||
if _, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "shop",
|
||||
Invokes: []string{"telegram"}, PasswordHash: "x"}); err == nil {
|
||||
t.Fatal("a grant naming a module but no tool was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// What a declaration says reaches the composed user, so a manifest's `invokes` is the grant.
|
||||
func TestADeclaredInvokeReachesTheComposedUser(t *testing.T) {
|
||||
users, err := Users(Records{
|
||||
Nodes: []string{"desk"},
|
||||
Assigned: map[string][]Declared{"desk": {{Module: "mesh-console", Invokes: []string{"*"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
perms, err := PermissionsFor(users[len(users)-1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||
}
|
||||
+40
-14
@@ -70,12 +70,14 @@ type Principal struct {
|
||||
// a namespace no such module owns. Every service started and the graph stayed empty.
|
||||
Watches []Seat
|
||||
|
||||
// Invokes are the tools a person may call, as `<module>.<tool>`; a single `*` is every tool,
|
||||
// for an administrator. Only meaningful for KindPerson.
|
||||
// Invokes are the tools this principal may call, as `<module>.<tool>`; a single `*` is every
|
||||
// tool. A person's whole authority (design 25 §7), and a module's only if its manifest says so
|
||||
// (novox/hq ADR 0152) — the console's does, and nothing else's.
|
||||
//
|
||||
// **A list, not a role.** A person is not a module and holds no seat: nothing is addressed
|
||||
// to them, nothing is delivered to them, and they have no durable consumer to acknowledge.
|
||||
// What they have is permission to ask.
|
||||
// What they have is permission to ask. A module that invokes gains exactly the same
|
||||
// permission and nothing beside it.
|
||||
Invokes []string
|
||||
|
||||
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
||||
@@ -224,18 +226,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
case KindPerson:
|
||||
// Tools, and nothing else. Every subject a person may publish is a tool call; a person
|
||||
// who could publish an event would be able to claim a module said something.
|
||||
for _, t := range p.Invokes {
|
||||
if t == "*" {
|
||||
pub = append(pub, "mesh.mod.*.tool.>")
|
||||
continue
|
||||
}
|
||||
module, tool, ok := strings.Cut(t, ".")
|
||||
if !ok {
|
||||
return Permissions{}, fmt.Errorf(
|
||||
"%q does not name a tool: a person invokes <module>.<tool>, or * for every one", t)
|
||||
}
|
||||
pub = append(pub, "mesh.mod."+module+".tool."+tool)
|
||||
invoked, err := invokedSubjects(p.Invokes)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
|
||||
case KindEnrolment:
|
||||
// A leaked token is useless for anything but enrolling: it cannot read a declaration, hear
|
||||
@@ -293,6 +288,16 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// still granted per tool, by name, on the publish side.
|
||||
sub = append(sub, own+".tool.>")
|
||||
|
||||
// 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same
|
||||
// grant a person gets and derived the same way, so "what may this module ask" is
|
||||
// answered by the one list that answers it for everybody. Publish only: an answer
|
||||
// arrives on its own inbox, which every principal has below.
|
||||
invoked, err := invokedSubjects(p.Invokes)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
|
||||
// 2. What it consumes, by the emitter's own subject — an event is addressed to its
|
||||
// emitter, because the emitter's identity is the meaning (ADR 0118).
|
||||
for _, c := range p.Consumes {
|
||||
@@ -601,3 +606,24 @@ func quoted(values []string) string {
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// invokedSubjects is the publish side of a grant to call tools: one subject per `<module>.<tool>`,
|
||||
// or the whole tool namespace for `*`. A person's authority and a module's `invokes` are both this
|
||||
// (novox/hq ADR 0152), so a malformed entry is refused in one place, before it could be widened into
|
||||
// something that happens to parse.
|
||||
func invokedSubjects(invokes []string) ([]string, error) {
|
||||
var out []string
|
||||
for _, t := range invokes {
|
||||
if t == "*" {
|
||||
out = append(out, "mesh.mod.*.tool.>")
|
||||
continue
|
||||
}
|
||||
module, tool, ok := strings.Cut(t, ".")
|
||||
if !ok || module == "" || tool == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%q does not name a tool: one invokes <module>.<tool>, or * for every one", t)
|
||||
}
|
||||
out = append(out, "mesh.mod."+module+".tool."+tool)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ type Declared struct {
|
||||
Uses []Seat
|
||||
// Watches are the seats whose events it consumes.
|
||||
Watches []Seat
|
||||
// Invokes are the tools it calls, `<module>.<tool>` or `*` (novox/hq ADR 0152).
|
||||
Invokes []string
|
||||
}
|
||||
|
||||
// Records is what composing a user list needs to know about the mesh, and nothing more.
|
||||
@@ -61,7 +63,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
out = append(out, Principal{
|
||||
Kind: KindModule, Node: node, Module: d.Module,
|
||||
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
|
||||
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches,
|
||||
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A manifest may say which tools its module calls (novox/hq ADR 0152), and the parser accepts the
|
||||
// two shapes the grant has: a named tool, and every tool.
|
||||
func TestAManifestMaySayWhatItInvokes(t *testing.T) {
|
||||
m, err := ParseManifest([]byte(`{"module":"mesh-console","version":"1",` +
|
||||
`"invokes":["mesh-catalog.catalog_modules","*"]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m.Invokes) != 2 || m.Invokes[1] != "*" {
|
||||
t.Fatalf("invokes not read: %v", m.Invokes)
|
||||
}
|
||||
}
|
||||
|
||||
// An entry that names a module and no tool is refused at parse, in the manifest's words, rather than
|
||||
// at the composition of the bus's user list where it would stop the file for everybody.
|
||||
func TestAnInvokeThatNamesNoToolIsRefusedAtParse(t *testing.T) {
|
||||
_, err := ParseManifest([]byte(`{"module":"mesh-console","version":"1","invokes":["shop"]}`))
|
||||
if err == nil {
|
||||
t.Fatal("an invoke naming no tool was accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), `invokes "shop", which does not name a tool`) {
|
||||
t.Fatalf("refused for the wrong reason: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,11 @@ var renamed = map[string]string{
|
||||
|
||||
var name = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$`)
|
||||
|
||||
// toolName is what a module calls one of its tools: the sdk's tools are `catalog_modules` and
|
||||
// `gitea_list_repos`, so an underscore is ordinary here and a dot is not — the dot is what separates
|
||||
// the module from the tool in `<module>.<tool>`, and a tool name carrying one would be two grants.
|
||||
var toolName = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`)
|
||||
|
||||
// Claim is a singular resource a module takes over.
|
||||
type Claim struct {
|
||||
Name string `json:"name"`
|
||||
@@ -247,6 +252,16 @@ type Manifest struct {
|
||||
// module claiming a seat answers what that seat's protocol promises (novox/hq ADR 0118).
|
||||
Tools []string `json:"tools,omitempty"`
|
||||
|
||||
// Invokes are the tools this module calls, each `<module>.<tool>`, or the single entry `*` for
|
||||
// every tool on the mesh (novox/hq ADR 0152).
|
||||
//
|
||||
// **A grant, and only a grant.** The bus lets this module publish exactly those tool subjects
|
||||
// and nothing beside them — no event, no subscription, no seat. A module that declares none
|
||||
// calls nothing, which is every module but the console today. ADR 0095 made the control plane
|
||||
// the one caller and deferred this until a consumer asked; the console is that consumer, and a
|
||||
// person's account (design 25 §7) already had the same shape.
|
||||
Invokes []string `json:"invokes,omitempty"`
|
||||
|
||||
// Capabilities the machine must have. A different field from Requires because the remedy
|
||||
// differs: a missing module can be assigned, and a missing capability means the wrong
|
||||
// machine.
|
||||
@@ -1290,6 +1305,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
"%s listens on %d over %q, which is tcp or udp", m.Module, l.Port, p))
|
||||
}
|
||||
}
|
||||
problems = append(problems, invokeProblems(m)...)
|
||||
problems = append(problems, endpointNameProblems(m)...)
|
||||
problems = append(problems, RouteProblems(m)...)
|
||||
for _, port := range m.Guards {
|
||||
@@ -1766,3 +1782,25 @@ func EndpointPort(m Manifest, name string) (int, bool) {
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// invokeProblems judges what a module says it calls (novox/hq ADR 0152).
|
||||
//
|
||||
// Refused here, in the manifest's words, rather than at the next composition of the bus's user
|
||||
// list — where a bad entry would stop the whole file being written for everybody, as a person's
|
||||
// malformed grant would have (operator.go). An entry that names a module and no tool is the one
|
||||
// mistake worth naming: `shop` reads like a grant to a module's tools and would be a grant to nothing.
|
||||
func invokeProblems(m Manifest) []string {
|
||||
var problems []string
|
||||
for _, t := range m.Invokes {
|
||||
if t == "*" {
|
||||
continue
|
||||
}
|
||||
module, tool, named := strings.Cut(t, ".")
|
||||
if !named || !name.MatchString(module) || !toolName.MatchString(tool) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s invokes %q, which does not name a tool: a module invokes <module>.<tool>, or "+
|
||||
"* for every tool on the mesh (novox/hq ADR 0152)", m.Module, t))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
// The tools it answers, which is `tools` and not `serves`: the manifest's `serves` is the
|
||||
// facts a consumer needs to reach a provision, a different meaning under a similar word.
|
||||
Serves: m.Tools,
|
||||
// And what it calls (novox/hq ADR 0152) — the console's `*`, nothing else's.
|
||||
Invokes: m.Invokes,
|
||||
}
|
||||
for _, c := range m.Claims {
|
||||
// Every seat with a protocol, the mesh's own included. One that says only who does a job is
|
||||
|
||||
Reference in New Issue
Block a user