Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4470f42db | ||
|
|
4bb790bedf | ||
|
|
aa98729457 | ||
|
|
2fdb0516cd | ||
|
|
28a6858863 | ||
|
|
1ab0274547 | ||
|
|
07af86b153 | ||
|
|
dd2e7c8e64 | ||
|
|
756a3813e2 | ||
|
|
9a508701ff | ||
|
|
7ee7248fad |
@@ -3,7 +3,7 @@ module mesh-delivery-assigner
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-builder
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg=
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -128,9 +128,7 @@ func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) {
|
||||
delete(m.by, module)
|
||||
return
|
||||
}
|
||||
// The sandbox's composer may be the node-engine of an older delivery, which does not know the verdict waits: it is handed a
|
||||
// wait as the refusal it is held as.
|
||||
m.by[module] = v.AsRefused()
|
||||
m.by[module] = *v
|
||||
}
|
||||
|
||||
func (m *manifestVerdicts) all() map[string]delivery.ManifestVerdict {
|
||||
|
||||
@@ -170,10 +170,4 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) {
|
||||
if _, err := parseManifest(manifest); err != nil {
|
||||
t.Errorf("manifestReader: %v", err)
|
||||
}
|
||||
// The sandbox's composer may be an older node-engine, which knows no verdict waits: it is handed a wait as a refusal.
|
||||
var held manifestVerdicts
|
||||
held.put("nats", &delivery.ManifestVerdict{Module: "nats", State: delivery.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"})
|
||||
if v := held.all()["nats"]; v.State != delivery.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 {
|
||||
t.Errorf("a waiting verdict is handed to the sandbox as %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg=
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Command mesh-delivery-indexer-tools serves the indexer's doing tool (novox/hq issue 1945): the runtime launches it for a
|
||||
// call. indexer_take_in writes one value of the indexer's takeins state and waits for the indexer's answer there. The
|
||||
// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge asks the same way, and
|
||||
// indexer_verdicts reads the indexer's verdicts state. The read tools of the indexer's states are
|
||||
// indexer is the other program: it serves no request subject (ADR 0437 §4). The read tools of the indexer's states are
|
||||
// mesh-delivery-view's (indexer_module, indexer_modules, indexer_candidate, indexer_candidates).
|
||||
package main
|
||||
|
||||
@@ -22,10 +21,23 @@ import (
|
||||
// asker writes a takeins value and waits for the indexer's answer to it.
|
||||
type asker func(key string, value []byte) (index.TakeInAsk, error)
|
||||
|
||||
// Tools are the indexer's tools. read is the indexer's state of verdicts, which indexer_verdicts reads.
|
||||
func Tools(ask asker, now func() time.Time, read stdio.KeptState) []stdio.Tool {
|
||||
// Tools are the indexer's tools.
|
||||
func Tools(ask asker, now func() time.Time, counts func() (index.OldSectionsCount, error)) []stdio.Tool {
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
|
||||
return []stdio.Tool{
|
||||
{Name: index.OldSectionsTool,
|
||||
Description: "How many accepted modules use each old section of a manifest (state, logs, streams, objects), the foundation " +
|
||||
"(nats, mesh-grants, the indexer) left out: the count the judge refuses a use of a section at 0 by, as modules move " +
|
||||
"their bus state into `requires` by template. Read from the indexer's oldsections state, kept as the indexer judges " +
|
||||
"the manifests; an empty answer means the indexer has not judged since it started.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
||||
Run: func(map[string]any) (any, error) {
|
||||
c, err := counts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Counts, nil
|
||||
}},
|
||||
{Name: index.TakeInTool,
|
||||
Description: "Take a registered module in now: the indexer reads its manifest from the repository and commit the controller's " +
|
||||
"build records register it from (at the head the gate's source names, when it names one), whatever it tried before, and " +
|
||||
@@ -50,50 +62,6 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState) []stdio.Tool {
|
||||
}
|
||||
return a.Result, nil
|
||||
}},
|
||||
{Name: index.VerdictsTool,
|
||||
Description: "The indexer's verdicts on manifests that wait or are refused (all: every verdict), each as its verdicts state holds it: " +
|
||||
"the module, the digest, the state (waits: the manifest carries a field the indexer's judge does not know yet, named in " +
|
||||
"unknown, and every reader holds the module as a refused one and keeps what runs; refused: the judge's problems), the " +
|
||||
"judge that made it and when. A module's name narrows it to that module. A waiting manifest is judged again at each " +
|
||||
"new indexer and refused only once the newest build of the indexer on the trunk still does not know the field; " +
|
||||
"indexer_judge judges now.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "only this module's verdicts"},
|
||||
"all": map[string]any{"type": "boolean", "description": "list accepted verdicts too"}}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
keys, err := read.Keys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the verdicts state cannot be listed: %w", err)
|
||||
}
|
||||
all, _ := args["all"].(bool)
|
||||
rows, unread := index.ListVerdicts(keys, func(key string) ([]byte, error) {
|
||||
e, err := read.Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
}, str(args, "module"), all)
|
||||
return map[string]any{"verdicts": rows, "unread": unread}, nil
|
||||
}},
|
||||
{Name: index.JudgeAgainTool,
|
||||
Description: "Judge the manifests again now, as the indexer does at its start and after each merge, and answer how the module's " +
|
||||
"verdict stands (waits, refused or accepted, and why). For a manifest that waits for a judge that knows its fields, or " +
|
||||
"after a manifest was mended where the indexer has not heard it. A waiting manifest becomes refused here only when the " +
|
||||
"running indexer is the newest build of itself the trunk holds. indexer_verdicts reads the result.",
|
||||
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "the module's name, as the catalogue registers it"},
|
||||
"why": map[string]any{"type": "string", "description": "why it is judged again now, in plain words"}}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
key, value, err := index.NewJudgeAgain(str(args, "module"), str(args, "why"), stdio.CallerOf(args), now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a, err := ask(key, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Result, nil
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,8 +118,22 @@ func askTakeIn(s stdio.KeptState, bound time.Duration) asker {
|
||||
}
|
||||
}
|
||||
|
||||
// heldCounts reads the count of the old sections the indexer keeps.
|
||||
func heldCounts(s stdio.KeptState) func() (index.OldSectionsCount, error) {
|
||||
return func() (index.OldSectionsCount, error) {
|
||||
e, err := s.Get(index.OldSectionsKey)
|
||||
if err != nil {
|
||||
return index.OldSectionsCount{}, fmt.Errorf("reading %s.%s: %w", index.OldSectionsBucket, index.OldSectionsKey, err)
|
||||
}
|
||||
if e == nil {
|
||||
return index.OldSectionsCount{}, fmt.Errorf("the indexer has not counted the old sections yet: %s holds no %s", index.OldSectionsBucket, index.OldSectionsKey)
|
||||
}
|
||||
return index.DecodeOldSections(e.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState))); err != nil {
|
||||
if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, heldCounts(stdio.State(index.OldSectionsState)))); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -296,12 +296,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// The published packages (ADR 0593 §2): a state this delivery brings, bound when it appears; until then every
|
||||
// fingerprint takes the version line.
|
||||
packages := process.AwaitBucket(ctx, js, conn, delivery.PackagesBucket, "no package is published, so every fingerprint takes the version line", logger.Printf)
|
||||
ix := &index.Indexer{Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known,
|
||||
Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf),
|
||||
// How many modules use each old section (issue 1966 D4): a state this delivery brings, bound when it appears; until then
|
||||
// the judge refuses no use of an old section.
|
||||
oldSections := process.AwaitBucket(ctx, js, conn, index.OldSectionsBucket, "the count of the old sections is not kept, so the judge refuses no use of one", logger.Printf)
|
||||
ix := &index.Indexer{OldSections: awaitedStore{oldSections}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known,
|
||||
Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), SayAccepted: sayAccepted(js, cred, logger.Printf),
|
||||
Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf,
|
||||
Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call),
|
||||
ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call),
|
||||
OwnBuild: ownBuildVia(calls.Call, cred.Node)}
|
||||
ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call)}
|
||||
// The log's entries at the start are read before any fingerprint is computed or registered module indexed (below); each
|
||||
// new one indexes the module it names when that module is registered and the bucket holds none (registered.go).
|
||||
var started atomic.Bool
|
||||
@@ -422,8 +424,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
logger.Printf("judging the manifests at start: %v; judged again at the next message heard", err)
|
||||
return
|
||||
}
|
||||
logger.Printf("judged %d manifests at start: %d verdicts written, %d with problems, of which outgrown and kept accepted: %v, waiting for a judge that knows their fields: %v",
|
||||
j.Judged, j.Written, len(j.Refused), j.Outgrown, j.Waiting)
|
||||
logger.Printf("judged %d manifests at start: %d verdicts written, %d with problems, of which outgrown and kept accepted: %v",
|
||||
j.Judged, j.Written, len(j.Refused), j.Outgrown)
|
||||
})
|
||||
// A build module says each time it wrote its toolchains key: its stream, and the indexer's durable consumer on it, are
|
||||
// made by the controller as the forge's are. Where the stream is not there yet the indexer says so and is started again
|
||||
|
||||
@@ -50,19 +50,6 @@ func TestTheTrunkIsTheDirectoriesTheForgeListsTrees(t *testing.T) { // 1900/AC3
|
||||
if _, has := gotArgs.(map[string]any)["paths"]; has {
|
||||
t.Error("the trunk asked for paths")
|
||||
}
|
||||
// The build the indexer runs is the engine's word (Runs.FP of its composition verb), asked of its own machine.
|
||||
var asked string
|
||||
own := func(key string, body any) (json.RawMessage, error) {
|
||||
asked = key
|
||||
return json.RawMessage(`{"runs":{"fp":"src2:abc"}}`), nil
|
||||
}
|
||||
if fp, err := ownBuildVia(own, "n1")(context.Background()); err != nil || fp != "src2:abc" || asked != "seat:node-engine.composition@n1" {
|
||||
t.Errorf("own build = %q %v asked %q", fp, err, asked)
|
||||
}
|
||||
none := func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }
|
||||
if _, err := ownBuildVia(none, "n1")(context.Background()); err == nil {
|
||||
t.Error("an engine that says nothing runs gave a build")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadViaReadsManifestsAndLocks(t *testing.T) {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// compositionVerb is the node-engine seat's read verb that says what runs of a module on a machine (its Runs.FP), asked of
|
||||
// that machine's own engine as `seat:node-engine.composition@<machine>`. The indexer learns the build it runs from it, never
|
||||
// from the trunk's value of itself or the haves bucket.
|
||||
const compositionVerb = "seat:node-engine.composition"
|
||||
|
||||
// ownBuildVia asks the engine of node which fingerprint of the indexer runs there.
|
||||
func ownBuildVia(call toolCaller, node string) func(ctx context.Context) (string, error) {
|
||||
return func(context.Context) (string, error) {
|
||||
raw, err := call(compositionVerb+"@"+node, map[string]any{"module": delivery.IndexerModule})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", compositionVerb, err)
|
||||
}
|
||||
var a struct {
|
||||
Runs *struct {
|
||||
FP string `json:"fp"`
|
||||
} `json:"runs"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return "", fmt.Errorf("the answer of %s is not a composition: %w", compositionVerb, err)
|
||||
}
|
||||
if a.Runs == nil || a.Runs.FP == "" {
|
||||
return "", errors.New("the engine says no fingerprint of the indexer runs")
|
||||
}
|
||||
return a.Runs.FP, nil
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
// takerIn is the part of the indexer a takeins value asks through.
|
||||
type takerIn interface {
|
||||
TakeIn(ctx context.Context, module string) index.TakenIn
|
||||
JudgeAgain(ctx context.Context, module string) index.TakenIn
|
||||
}
|
||||
|
||||
// takenIn answers one value of the takeins state (the tool indexer_take_in, issue 1945): a value not yet answered takes the
|
||||
@@ -32,14 +31,9 @@ func takenIn(ctx context.Context, ix takerIn, kv jetstream.KeyValue, e jetstream
|
||||
if !a.Answered.IsZero() {
|
||||
return
|
||||
}
|
||||
asked, r := "take-in", index.TakenIn{}
|
||||
if a.Judge {
|
||||
asked, r = "judging again", ix.JudgeAgain(ctx, a.Module)
|
||||
} else {
|
||||
r = ix.TakeIn(ctx, a.Module)
|
||||
}
|
||||
r := ix.TakeIn(ctx, a.Module)
|
||||
a.Result, a.Answered = &r, time.Now().UTC()
|
||||
logf("%s: a %s asked by %s (%s): %s", a.Module, asked, a.By, a.Why, r.Answer)
|
||||
logf("%s: a take-in asked by %s (%s): %s", a.Module, a.By, a.Why, r.Answer)
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -36,3 +36,29 @@ func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sayAccepted says the indexer's event manifest-accepted (issue 1966 D3, novox/hq ADR 0595 §4):
|
||||
// mesh.mod.<module>.event.manifest-accepted, carrying the module's bus-state entries whole, with the digest and judge as
|
||||
// the bus's dedup id (delivery.ManifestAccepted.EventID), so one acceptance is said once.
|
||||
func sayAccepted(js jetstream.JetStream, cred process.Account, logf func(string, ...any)) func(context.Context, delivery.ManifestAccepted) {
|
||||
return func(ctx context.Context, a delivery.ManifestAccepted) {
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
logf("judge: %s for %s is not said: %v", delivery.ManifestAcceptedEvent, a.Module, err)
|
||||
return
|
||||
}
|
||||
msg := nats.NewMsg(cred.EventSubject(delivery.ManifestAcceptedEvent))
|
||||
id := a.EventID()
|
||||
msg.Header.Set("x-event-id", id)
|
||||
msg.Header.Set("x-source", cred.Module)
|
||||
msg.Header.Set("x-node", cred.Node)
|
||||
msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano))
|
||||
msg.Header.Set("content-type", "application/json")
|
||||
msg.Data = data
|
||||
pub, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
||||
defer cancel()
|
||||
if _, err := js.PublishMsg(pub, msg, jetstream.WithMsgID(id)); err != nil {
|
||||
logf("judge: %s", fmt.Errorf("saying %s for %s: %w", delivery.ManifestAcceptedEvent, a.Module, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 h1:T66tg9T/1hHg6KN/bDScHPT41IHkxLAGGO8VJiAOlb8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -54,10 +54,16 @@ type Indexer struct {
|
||||
Taken Store
|
||||
// Verdicts is the verdicts state (verdicts.go, novox/hq ADR 0578 §1); nil judges nothing.
|
||||
Verdicts Store
|
||||
// OldSections is the state of how many accepted modules use each old section (oldsections.go, issue 1966 D4); nil keeps no
|
||||
// count and the judge refuses no use of an old section.
|
||||
OldSections Store
|
||||
// JudgeID names the judge a verdict says made it; empty is judge.Identity, the running program's.
|
||||
JudgeID string
|
||||
// SayRefused says the event manifest-refused for a refusal newly written; nil says nothing.
|
||||
SayRefused func(ctx context.Context, r delivery.ManifestRefused)
|
||||
// SayAccepted says the event manifest-accepted for an accepted verdict newly written whose manifest asks for bus state,
|
||||
// carrying the entries whole (verdicts.go; issue 1966 D3); nil says nothing.
|
||||
SayAccepted func(ctx context.Context, a delivery.ManifestAccepted)
|
||||
// Packages is the packages state (delivery.PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none,
|
||||
// and every fingerprint takes the version line.
|
||||
Packages Store
|
||||
@@ -92,27 +98,22 @@ type Indexer struct {
|
||||
// closed message the indexer missed heals at the next one (issue 1126); nil asks none.
|
||||
ReadOpen func(ctx context.Context) ([]delivery.OpenPull, error)
|
||||
|
||||
mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach
|
||||
source sourceNow
|
||||
previews map[string]delivery.Preview // by previews key (previews.go)
|
||||
toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go)
|
||||
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
|
||||
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
|
||||
moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
|
||||
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
|
||||
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
|
||||
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
|
||||
recompute sync.Mutex // one Recompute at a time
|
||||
untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go)
|
||||
filling atomic.Bool // a FillTaken runs in the background (KickFill)
|
||||
fillAgain atomic.Bool // a kick arrived while it ran
|
||||
atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished)
|
||||
// OwnBuild answers the fingerprint of the build this program runs, from the node-engine's seat; nil knows none.
|
||||
OwnBuild func(ctx context.Context) (string, error)
|
||||
judgeMu sync.Mutex // one judge pass at a time (verdicts.go)
|
||||
ownMu sync.Mutex // guards ownFP
|
||||
ownFP string // the fingerprint of the build this program runs, asked at the start pass (learnOwnBuild)
|
||||
judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go)
|
||||
mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach
|
||||
source sourceNow
|
||||
previews map[string]delivery.Preview // by previews key (previews.go)
|
||||
toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go)
|
||||
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
|
||||
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
|
||||
moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
|
||||
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
|
||||
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
|
||||
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
|
||||
recompute sync.Mutex // one Recompute at a time
|
||||
untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go)
|
||||
filling atomic.Bool // a FillTaken runs in the background (KickFill)
|
||||
fillAgain atomic.Bool // a kick arrived while it ran
|
||||
atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished)
|
||||
judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go)
|
||||
}
|
||||
|
||||
func (ix *Indexer) logf(format string, args ...any) {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-indexer/judge"
|
||||
)
|
||||
|
||||
// How many accepted modules use each old section (issue 1966 D4; novox/hq ADR 0595 §11): the indexer counts them, the
|
||||
// foundation left out, as it judges, keeps the count in its state oldsections under one key, and the tool
|
||||
// indexer_old_sections reads it. The judge refuses a manifest of another module that uses a section whose count is 0.
|
||||
const (
|
||||
// OldSectionsTool is the tool that reads the count.
|
||||
OldSectionsTool = "indexer_old_sections"
|
||||
// OldSectionsState is the indexer's state of the count: the bucket mesh-delivery-indexer_oldsections.
|
||||
OldSectionsState = "oldsections"
|
||||
OldSectionsBucket = delivery.IndexerModule + "_" + OldSectionsState
|
||||
// OldSectionsKey is the one key it holds.
|
||||
OldSectionsKey = "counts"
|
||||
)
|
||||
|
||||
// OldSectionsCount is the value of the oldsections state: per old section, the accepted modules outside the foundation using it.
|
||||
type OldSectionsCount struct {
|
||||
V int `json:"v"`
|
||||
Counts map[string]int `json:"counts"`
|
||||
}
|
||||
|
||||
// DecodeOldSections reads the oldsections value.
|
||||
func DecodeOldSections(value []byte) (OldSectionsCount, error) {
|
||||
var c OldSectionsCount
|
||||
if err := json.Unmarshal(value, &c); err != nil {
|
||||
return c, fmt.Errorf("the count of the old sections cannot be read: %w", err)
|
||||
}
|
||||
if c.V != 1 {
|
||||
return c, fmt.Errorf("a count of the old sections of version %d, not 1", c.V)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// heldOldSections is the count the last pass wrote (the accepted set it held): a module that keeps using a section was
|
||||
// accepted with it, so it is counted, and a fresh manifest changes nothing in it. Nil when none is held yet or it cannot be
|
||||
// read; a failed read is said, never taken as 0 (issue 1966 D4).
|
||||
func (ix *Indexer) heldOldSections(ctx context.Context) judge.OldSectionCounts {
|
||||
value, _, found, err := ix.OldSections.Get(ctx, OldSectionsKey)
|
||||
if err == nil && found {
|
||||
var c OldSectionsCount
|
||||
if c, err = DecodeOldSections(value); err == nil {
|
||||
return c.Counts
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
ix.logf("judge: the count of the old sections cannot be read (%v), so the zero rule is not applied in this pass", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// keepOldSections writes the counts under OldSectionsKey when they differ from the ones held.
|
||||
func (ix *Indexer) keepOldSections(ctx context.Context, counts judge.OldSectionCounts) error {
|
||||
if ix.OldSections == nil {
|
||||
return nil
|
||||
}
|
||||
return write(ctx, ix.OldSections, OldSectionsKey, func(value []byte, found bool) ([]byte, bool, error) {
|
||||
if found {
|
||||
if cur, err := DecodeOldSections(value); err == nil && maps.Equal(cur.Counts, counts) {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
next, err := json.Marshal(OldSectionsCount{V: 1, Counts: counts})
|
||||
return next, false, err
|
||||
})
|
||||
}
|
||||
@@ -25,11 +25,9 @@ const (
|
||||
// TakeInAsk is a value of the takeins state: which module a person asked to be taken in, why, who and when, and, once the
|
||||
// indexer read it, its answer.
|
||||
type TakeInAsk struct {
|
||||
V int `json:"v"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
// Judge asks the manifests to be judged again now (indexer_judge_again) instead of the module to be taken in.
|
||||
Judge bool `json:"judge,omitempty"`
|
||||
V int `json:"v"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
Result *TakenIn `json:"result,omitempty"`
|
||||
|
||||
@@ -353,7 +353,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
|
||||
if held && cur.Manifest == next.Manifest {
|
||||
// The judge's verdict stands while the digest does (ADR 0578 §3, ADR 0579 §1): a recompute never writes a
|
||||
// refused or outgrown module as accepted; only the judge changes it.
|
||||
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
|
||||
next.Verdict, next.Problems, next.Warnings = cur.Verdict, cur.Problems, cur.Warnings
|
||||
}
|
||||
if held && sameButWhere(cur, next) {
|
||||
return nil, false, nil
|
||||
@@ -380,7 +380,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
|
||||
func sameButWhere(cur, next delivery.Module) bool {
|
||||
next = withV(next)
|
||||
next.Commit, next.Src = cur.Commit, cur.Src
|
||||
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
|
||||
next.Verdict, next.Problems, next.Warnings = cur.Verdict, cur.Problems, cur.Warnings
|
||||
return reflect.DeepEqual(cur, next)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -20,23 +18,19 @@ import (
|
||||
// The indexer judges every manifest it holds, once, with its own judge (novox/hq ADR 0578 §1): it writes each verdict in
|
||||
// its state verdicts (delivery.VerdictsBucket), one key per manifest digest, only when the verdict differs from the one
|
||||
// held; a module's value in the modules state carries a refusal (Verdict, Problems), so a reader of modules takes it
|
||||
// without a second read; a new refusal is said as the event manifest-refused; a manifest with a field the judge does not know waits instead (ownBuildIsNewest). It judges at its start, so a judge that
|
||||
// without a second read; a new refusal is said as the event manifest-refused. It judges at its start, so a judge that
|
||||
// changed (a new indexer) judges every manifest again, and after every merge it takes in, since a rule between manifests
|
||||
// can turn on any of them. Nothing polls.
|
||||
|
||||
// VerdictsBucket is the indexer's state of verdicts; VerdictsState is its local name, as the tools bundle reads it.
|
||||
// VerdictsBucket is the indexer's state of verdicts.
|
||||
var VerdictsBucket = delivery.VerdictsBucket
|
||||
|
||||
const VerdictsState = "verdicts"
|
||||
|
||||
// Judged is what one pass of the judge did: the manifests judged, the verdicts written, and the modules refused.
|
||||
type Judged struct {
|
||||
Judged, Written int
|
||||
Refused map[string][]string
|
||||
// Outgrown are the modules accepted before whose problems are now warnings (ADR 0579 §1).
|
||||
Outgrown []string
|
||||
// Waiting are the modules whose manifest carries a field this judge does not know and waits.
|
||||
Waiting []string
|
||||
}
|
||||
|
||||
// JudgeAll judges every manifest the modules state names, as one catalogue with the predefined list the indexer keeps,
|
||||
@@ -49,10 +43,6 @@ type Judged struct {
|
||||
// watchman raises as manifest-outgrown. A manifest in fresh (the digests a merge just took in) with no verdict yet, and
|
||||
// one already refused, is judged in full.
|
||||
func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, error) {
|
||||
// A merge, the start pass and indexer_judge may all judge at once: one pass at a time, so none writes a verdict from
|
||||
// modules another is rewriting.
|
||||
ix.judgeMu.Lock()
|
||||
defer ix.judgeMu.Unlock()
|
||||
var out Judged
|
||||
if ix.Verdicts == nil || ix.Modules == nil || ix.Manifests == nil {
|
||||
return out, nil
|
||||
@@ -84,37 +74,26 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
raws[name] = []byte(kept.JSON)
|
||||
digests[name] = digest
|
||||
}
|
||||
refused, waits, unnamed := judge.ShelfWaiting(raws, ix.predefinedList(ctx), 0)
|
||||
predefined := ix.predefinedList(ctx)
|
||||
// The old sections end when no module is left in them (issue 1966 D4): the count is the accepted set the last pass held.
|
||||
var counts judge.OldSectionCounts
|
||||
if ix.OldSections != nil {
|
||||
counts = ix.heldOldSections(ctx)
|
||||
}
|
||||
refused, unnamed := judge.ShelfCounting(raws, predefined, 0, counts)
|
||||
for _, p := range unnamed {
|
||||
ix.logf("judge: a problem between the manifests that no one module's removal ends, so it refuses none: %s", p)
|
||||
}
|
||||
out.Refused = refused
|
||||
// A manifest with a field this judge does not know waits for a judge that may know it (clause 3 of the record on a
|
||||
// release's Approve; ADR 0578 §1): it is refused only when this program is the newest build of the indexer the trunk
|
||||
// holds, so no judge that could know the field is still to come.
|
||||
newest := ix.ownBuildIsNewest(held)
|
||||
id := ix.JudgeID
|
||||
if id == "" {
|
||||
id = judge.Identity()
|
||||
}
|
||||
var accepted []catalogue.Manifest
|
||||
for _, name := range slices.Sorted(maps.Keys(digests)) {
|
||||
out.Judged++
|
||||
v := delivery.ManifestVerdict{Module: name, State: delivery.VerdictAccepted, Judge: id, At: ix.now()}
|
||||
if fields := waits[name]; len(fields) > 0 {
|
||||
accepted := ix.acceptedBefore(ctx, digests[name], fresh)
|
||||
if !newest && !accepted {
|
||||
v.State, v.Unknown = delivery.VerdictManifestWaits, fields
|
||||
out.Waiting = append(out.Waiting, name)
|
||||
} else {
|
||||
_, problems := judge.Manifest(raws[name], ix.predefinedList(ctx))
|
||||
if accepted {
|
||||
v.Warnings = problems
|
||||
out.Outgrown = append(out.Outgrown, name)
|
||||
} else {
|
||||
v.State, v.Problems = delivery.VerdictRefused, problems
|
||||
}
|
||||
}
|
||||
} else if problems := refused[name]; len(problems) > 0 {
|
||||
if problems := refused[name]; len(problems) > 0 {
|
||||
if ix.acceptedBefore(ctx, digests[name], fresh) {
|
||||
v.Warnings = problems
|
||||
out.Outgrown = append(out.Outgrown, name)
|
||||
@@ -122,6 +101,12 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
v.State, v.Problems = delivery.VerdictRefused, problems
|
||||
}
|
||||
}
|
||||
if !v.Refused() {
|
||||
if m, err := catalogue.Decode(raws[name]); err == nil {
|
||||
m.Module = name
|
||||
accepted = append(accepted, m)
|
||||
}
|
||||
}
|
||||
wrote, err := ix.writeVerdict(ctx, digests[name], v)
|
||||
if errors.Is(err, process.ErrNotYet) {
|
||||
return out, fmt.Errorf("the state %s is not on the bus yet: %w", VerdictsBucket, err)
|
||||
@@ -132,27 +117,31 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
}
|
||||
if wrote {
|
||||
out.Written++
|
||||
if v.Waits() {
|
||||
ix.logf("judge: %s", v.Why())
|
||||
}
|
||||
if v.Refused() {
|
||||
ix.logf("judge: %s", v.Why())
|
||||
if ix.SayRefused != nil {
|
||||
ix.SayRefused(ctx, delivery.RefusedOf(digests[name], v))
|
||||
}
|
||||
} else if ix.SayAccepted != nil {
|
||||
// A manifest asking for bus state, newly accepted, is said whole to the bus module (issue 1966 D3, ADR 0595 §4).
|
||||
if m, problems := judge.Manifest(raws[name], predefined); len(problems) == 0 && len(m.BusState) > 0 {
|
||||
ix.SayAccepted(ctx, delivery.AcceptedOf(digests[name], v, m))
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ix.carryVerdict(ctx, name, digests[name], v); err != nil {
|
||||
ix.logf("judge: modules.%s does not carry its verdict: %v", name, err)
|
||||
}
|
||||
}
|
||||
if err := ix.keepOldSections(ctx, judge.CountOldSections(accepted)); err != nil {
|
||||
ix.logf("judge: the count of the old sections is not kept: %v", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// JudgeAtStart is JudgeAll of every manifest held, none fresh, as the indexer starts (a judge that changed judges every
|
||||
// one again); one that fails is judged again at the next message or source revision heard (kickJudge).
|
||||
func (ix *Indexer) JudgeAtStart(ctx context.Context) (Judged, error) {
|
||||
ix.learnOwnBuild(ctx)
|
||||
j, err := ix.JudgeAll(ctx, nil)
|
||||
ix.judgePending.Store(err != nil)
|
||||
return j, err
|
||||
@@ -181,7 +170,7 @@ func (ix *Indexer) acceptedBefore(ctx context.Context, digest string, fresh map[
|
||||
return !fresh[digest]
|
||||
}
|
||||
prev, err := delivery.DecodeManifestVerdict(value)
|
||||
return err == nil && prev.State == delivery.VerdictAccepted
|
||||
return err == nil && !prev.Refused()
|
||||
}
|
||||
|
||||
// writeVerdict writes v under the digest's key unless the verdict held there says the same (ManifestVerdict.Same); wrote
|
||||
@@ -205,36 +194,6 @@ func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.M
|
||||
return wrote, err
|
||||
}
|
||||
|
||||
// learnOwnBuild asks where the indexer's program is told which build it runs (OwnBuild: the node-engine's seat, never the
|
||||
// trunk's value of the indexer, which names a build that may be merged and not delivered). Asked at the start pass; where
|
||||
// it is not answered the build stays unknown and every unknown field waits, which is the safe side.
|
||||
func (ix *Indexer) learnOwnBuild(ctx context.Context) {
|
||||
if ix.OwnBuild == nil {
|
||||
return
|
||||
}
|
||||
fp, err := ix.OwnBuild(ctx)
|
||||
if err != nil || fp == "" {
|
||||
ix.logf("judge: the build this program runs is not known (%v), so a manifest with a field it does not know waits and is not refused", err)
|
||||
return
|
||||
}
|
||||
ix.ownMu.Lock()
|
||||
ix.ownFP = fp
|
||||
ix.ownMu.Unlock()
|
||||
}
|
||||
|
||||
// ownBuildIsNewest says whether the build of the indexer this program runs (ownFP, from the node-engine) is the newest the
|
||||
// trunk holds: the indexer's own modules value names it. A newer build merged and not yet delivered ends it at the trunk's
|
||||
// merge, as one that pins a changed mesh-sdk always is; false when either is unknown.
|
||||
func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool {
|
||||
fp := held[delivery.IndexerModule].FP2
|
||||
if fp == "" {
|
||||
return false
|
||||
}
|
||||
ix.ownMu.Lock()
|
||||
defer ix.ownMu.Unlock()
|
||||
return ix.ownFP != "" && ix.ownFP == fp
|
||||
}
|
||||
|
||||
// carryVerdict puts a refusal into the module's value, and takes one out once the manifest is accepted, while the value
|
||||
// still names the digest judged; an accepted module's value carries nothing, so judging writes no value of a module the
|
||||
// judge accepts (a reader takes a value without a verdict as accepted, delivery.TakeManifestVerdict).
|
||||
@@ -251,18 +210,14 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli
|
||||
if err != nil || cur.Manifest != digest {
|
||||
return nil, false, nil // gone, unreadable, or moved to another manifest since: the next pass judges it
|
||||
}
|
||||
// A wait is written as a refusal naming the fields, so a reader pinned before the verdict waits holds it as refused.
|
||||
state, problems := "", v.Problems
|
||||
switch {
|
||||
case v.Waits():
|
||||
state, problems = delivery.VerdictRefused, []string{v.WaitsOn()}
|
||||
case v.Refused():
|
||||
state := ""
|
||||
if v.Refused() {
|
||||
state = delivery.VerdictRefused
|
||||
}
|
||||
if cur.Verdict == state && slices.Equal(cur.Problems, problems) && slices.Equal(cur.Unknown, v.Unknown) && slices.Equal(cur.Warnings, v.Warnings) {
|
||||
if cur.Verdict == state && slices.Equal(cur.Problems, v.Problems) && slices.Equal(cur.Warnings, v.Warnings) {
|
||||
return nil, false, nil
|
||||
}
|
||||
cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings = state, problems, v.Unknown, v.Warnings
|
||||
cur.Verdict, cur.Problems, cur.Warnings = state, v.Problems, v.Warnings
|
||||
if state == "" {
|
||||
cur.Problems = nil
|
||||
}
|
||||
@@ -292,111 +247,3 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// The tools that read and steer the verdicts (the instruction "tools are how a module is spoken to"): indexer_verdicts
|
||||
// lists the manifests that wait or are refused, and indexer_judge judges one module again now.
|
||||
const (
|
||||
VerdictsTool = "indexer_verdicts"
|
||||
JudgeAgainTool = "indexer_judge"
|
||||
judgeAgainPrefix = "judge."
|
||||
)
|
||||
|
||||
// VerdictRow is one verdict as indexer_verdicts lists it: the manifest's digest, the module, its state, the fields it waits
|
||||
// on or the problems that refuse it, and the judge that made it.
|
||||
type VerdictRow struct {
|
||||
Module string `json:"module"`
|
||||
Digest string `json:"digest"`
|
||||
State string `json:"state"`
|
||||
Words string `json:"words,omitempty"`
|
||||
Unknown []string `json:"unknown,omitempty"`
|
||||
Problems []string `json:"problems,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Judge string `json:"judge"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// ListVerdicts reads the verdicts under keys with get and lists those of module (every module when empty) that wait or are
|
||||
// refused, or every one with all, sorted by module and digest. A value that does not read is said in unread by its key.
|
||||
func ListVerdicts(keys []string, get func(key string) ([]byte, error), module string, all bool) (rows []VerdictRow, unread map[string]string) {
|
||||
unread = map[string]string{}
|
||||
for _, key := range keys {
|
||||
value, err := get(key)
|
||||
if err != nil || value == nil {
|
||||
if err != nil {
|
||||
unread[key] = err.Error()
|
||||
}
|
||||
continue
|
||||
}
|
||||
v, err := delivery.DecodeManifestVerdict(value)
|
||||
if err != nil {
|
||||
unread[key] = err.Error()
|
||||
continue
|
||||
}
|
||||
if (module != "" && v.Module != module) || (!all && !v.Held()) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, VerdictRow{Module: v.Module, Digest: key, State: v.State, Words: v.Why(), Unknown: v.Unknown,
|
||||
Problems: v.Problems, Warnings: v.Warnings, Judge: v.Judge, At: v.At})
|
||||
}
|
||||
slices.SortFunc(rows, func(a, b VerdictRow) int {
|
||||
if c := strings.Compare(a.Module, b.Module); c != 0 {
|
||||
return c
|
||||
}
|
||||
return strings.Compare(a.Digest, b.Digest)
|
||||
})
|
||||
return rows, unread
|
||||
}
|
||||
|
||||
// JudgeAgain judges the manifests again now, as at the start, and answers how the module's stands: the tool
|
||||
// indexer_judge. A module the modules state does not hold, or whose manifest is not in the manifests state, is
|
||||
// answered so.
|
||||
func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn {
|
||||
out := TakenIn{Module: module}
|
||||
held, err := ix.heldModules(ctx)
|
||||
if err != nil {
|
||||
out.Answer = "the modules state cannot be read: " + err.Error()
|
||||
return out
|
||||
}
|
||||
m, ok := held[module]
|
||||
if !ok || m.Manifest == "" {
|
||||
out.Answer = "the indexer holds no manifest of " + module
|
||||
return out
|
||||
}
|
||||
// The build this program runs is asked again, so a start pass that missed the engine's answer is recovered without a restart.
|
||||
ix.learnOwnBuild(ctx)
|
||||
if _, err := ix.JudgeAll(ctx, nil); err != nil {
|
||||
out.Answer = "the manifests could not be judged: " + err.Error()
|
||||
return out
|
||||
}
|
||||
value, _, found, err := ix.Verdicts.Get(ctx, m.Manifest)
|
||||
if err != nil || !found {
|
||||
out.Answer = "judged, but the verdict on " + module + " cannot be read"
|
||||
return out
|
||||
}
|
||||
v, err := delivery.DecodeManifestVerdict(value)
|
||||
if err != nil {
|
||||
out.Answer = "judged, but the verdict on " + module + " cannot be read: " + err.Error()
|
||||
return out
|
||||
}
|
||||
out.Stored = true
|
||||
out.Answer = "judged again by " + v.Judge + ": " + v.State
|
||||
if v.Held() {
|
||||
out.Answer += " (" + v.Why() + ")"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// NewJudgeAgain is the value indexer_judge writes for a module: its key in the takeins state and the value.
|
||||
func NewJudgeAgain(module, why, by string, at time.Time) (string, []byte, error) {
|
||||
key, data, err := NewTakeIn(module, why, by, at)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
var a TakeInAsk
|
||||
if err := json.Unmarshal(data, &a); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
a.Judge = true
|
||||
data, err = json.Marshal(a)
|
||||
return judgeAgainPrefix + key, data, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package index
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -119,88 +120,92 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil || !w.fresh[other] {
|
||||
t.Fatalf("a changed digest is not fresh: %v %v", w.fresh, err)
|
||||
}
|
||||
// Clause 3 of the record on a release's Approve: a manifest with a field this judge does not know waits (no refusal, no
|
||||
// event) while a newer build of the indexer is on the trunk, and is judged again at the next judge; once this program's
|
||||
// build is the trunk's newest it is refused; a wait is written into the module's value as a refusal naming the fields.
|
||||
ownFP := "src2:" + strings.Repeat("e", 64)
|
||||
hold(delivery.IndexerModule, `{"module":"mesh-delivery-indexer","version":"1"}`)
|
||||
ix.ownFP = ownFP // this program started when the trunk held another build of the indexer than the one it holds now
|
||||
later := hold("later", `{"module":"later","version":"1","a-field-from-later":1,"another":{"x":1}}`)
|
||||
said = nil
|
||||
before := len(said)
|
||||
j, err = ix.JudgeAll(ctx, map[string]bool{later: true})
|
||||
if err != nil || len(j.Waiting) != 1 || j.Waiting[0] != "later" || len(said) != before {
|
||||
t.Fatalf("a manifest with fields from later: %+v %v, said %d", j, err, len(said))
|
||||
// 1966/AC10: a newly accepted manifest asking for bus state is said once as manifest-accepted, carrying both entries
|
||||
// whole; a verdict already held, and a manifest asking for none, say nothing.
|
||||
var accepted []delivery.ManifestAccepted
|
||||
ix.SayAccepted = func(_ context.Context, a delivery.ManifestAccepted) { accepted = append(accepted, a) }
|
||||
moved := hold("moved", `{"module":"moved","version":"1","requires":[`+
|
||||
`{"name":"cache","storage":"key-value","template":"state","options":{"history":8}},`+
|
||||
`{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64}}]}`)
|
||||
for pass := 0; pass < 2; pass++ {
|
||||
if _, err := ix.JudgeAll(ctx, map[string]bool{moved: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if v := verdict(later); !v.Waits() || v.Refused() || strings.Join(v.Unknown, ",") != "a-field-from-later,another" {
|
||||
t.Fatalf("later: %+v", v)
|
||||
if len(accepted) != 1 || accepted[0].Module != "moved" || accepted[0].Digest != moved || accepted[0].Judge != "judge-2" ||
|
||||
len(accepted[0].BusState) != 2 || accepted[0].BusState[0].Name != "cache" || accepted[0].BusState[1].Template != "log" ||
|
||||
string(accepted[0].BusState[0].Options["history"]) != "8" {
|
||||
t.Fatalf("said accepted %+v", accepted)
|
||||
}
|
||||
if m := module("later"); m.Verdict != delivery.VerdictRefused || len(m.Unknown) != 2 || !m.VerdictOf("later").Waits() ||
|
||||
!strings.Contains(m.Problems[0], "does not know yet") {
|
||||
t.Fatalf("modules.later carries %+v", m)
|
||||
// 1966/AC11: modules using state and none using logs but the foundation: logs counts 0, so a manifest of another
|
||||
// module using logs is refused and the foundation's is accepted.
|
||||
ix.OldSections = newMem()
|
||||
hold("user", `{"module":"user","version":"1","state":["x"]}`)
|
||||
hold("mesh-grants", `{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)
|
||||
if _, err := ix.JudgeAll(ctx, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The build this program runs is asked of the engine, and the trunk moving before the start pass changes nothing: the
|
||||
// trunk's value of the indexer names a newer build than the one the engine says runs, so the field still waits.
|
||||
ix.ownFP = ""
|
||||
ix.OwnBuild = func(context.Context) (string, error) { return ownFP, nil }
|
||||
if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() {
|
||||
t.Fatalf("the trunk moved before the start pass: %v %+v", err, verdict(later))
|
||||
counts := func() map[string]int {
|
||||
value, _, found, err := ix.OldSections.Get(ctx, OldSectionsKey)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("no count of the old sections: %v", err)
|
||||
}
|
||||
c, err := DecodeOldSections(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c.Counts
|
||||
}
|
||||
ix.OwnBuild = func(context.Context) (string, error) { return "", errors.New("the engine does not answer") }
|
||||
ix.ownFP = ""
|
||||
if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() {
|
||||
t.Fatalf("an unknown build refused: %v %+v", err, verdict(later))
|
||||
if c := counts(); c["state"] != 1 || c["logs"] != 0 || c["streams"] != 0 || c["objects"] != 0 {
|
||||
t.Fatalf("the old sections count %v, want state 1 and the rest 0", c)
|
||||
}
|
||||
ix.ownFP = ownFP
|
||||
// A renamed field is a known mistake: refused at once.
|
||||
renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`)
|
||||
if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !verdict(renamed).Refused() {
|
||||
t.Fatalf("a renamed field: %+v %v", j, err)
|
||||
late := hold("late", `{"module":"late","version":"1","logs":[{"name":"l"}]}`)
|
||||
grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2","logs":[{"name":"l"},{"name":"m"}]}`)
|
||||
if _, err := ix.JudgeAll(ctx, map[string]bool{late: true, grants: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The trunk's newest build of the indexer is this program's: the field is one no judge to come knows.
|
||||
ix.ownFP = "src2:" + strings.Repeat("a", 64)
|
||||
if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Waiting) != 0 || !verdict(later).Refused() || !strings.Contains(verdict(later).Problems[0], "a-field-from-later") {
|
||||
t.Fatalf("the newest build refuses: %+v %v %+v", j, err, verdict(later))
|
||||
if v := verdict(late); !v.Refused() || !strings.Contains(v.Problems[0], "late uses the old section logs, which no other module uses any more") {
|
||||
t.Fatalf("late: %+v", v)
|
||||
}
|
||||
if m := module("later"); m.Verdict != delivery.VerdictRefused || m.VerdictOf("later").Waits() || len(m.Unknown) != 0 {
|
||||
t.Fatalf("modules.later after the refusal carries %+v", m)
|
||||
if v := verdict(grants); v.Refused() {
|
||||
t.Fatalf("the foundation's manifest was refused: %+v", v)
|
||||
}
|
||||
// The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge
|
||||
// judges now and says how the module's verdict stands.
|
||||
keys, _ := ix.Verdicts.Keys(ctx)
|
||||
get := func(key string) ([]byte, error) { v, _, _, err := ix.Verdicts.Get(ctx, key); return v, err }
|
||||
if rows, unread := ListVerdicts(keys, get, "later", false); len(rows) != 1 || rows[0].State != delivery.VerdictRefused || len(unread) != 0 {
|
||||
t.Fatalf("indexer_verdicts of later: %+v %v", rows, unread)
|
||||
// 1966/AC11: the sole user of an old section changes its manifest and keeps the section: accepted, since it was accepted
|
||||
// with it (the count is the accepted set held from the last pass, not recounted from fresh digests).
|
||||
hold("solo", `{"module":"solo","version":"1","objects":[{"name":"o","max-mib":1}]}`)
|
||||
if _, err := ix.JudgeAll(ctx, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rows, _ := ListVerdicts(keys, get, "", true); len(rows) <= len(keys)-3 || rows[0].Module > rows[len(rows)-1].Module {
|
||||
t.Fatalf("indexer_verdicts, all: %d rows of %d verdicts", len(rows), len(keys))
|
||||
solo2 := hold("solo", `{"module":"solo","version":"2","objects":[{"name":"o","max-mib":2}]}`)
|
||||
if _, err := ix.JudgeAll(ctx, map[string]bool{solo2: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// indexer_judge asks the engine for the running build again: a start pass that missed it is recovered (the trunk's value of
|
||||
// the indexer is the newer one, so the field waits; asked and answered with that same build it is refused).
|
||||
ix.ownFP = ""
|
||||
ix.OwnBuild = func(context.Context) (string, error) { return "src2:" + strings.Repeat("a", 64), nil }
|
||||
if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "refused") || ix.ownFP == "" {
|
||||
t.Fatalf("indexer_judge did not learn the running build: %+v", got)
|
||||
if v := verdict(solo2); v.Refused() {
|
||||
t.Fatalf("the sole user of objects, changing its manifest and keeping objects, was refused: %+v", v)
|
||||
}
|
||||
ix.ownFP = ownFP
|
||||
ix.OwnBuild = nil
|
||||
if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") {
|
||||
t.Fatalf("indexer_judge: %+v", got)
|
||||
// A count that cannot be read is never taken as 0: the zero rule is not applied in that pass and the log says so.
|
||||
var logged []string
|
||||
ix.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
|
||||
held := ix.OldSections
|
||||
ix.OldSections = unreadable{held}
|
||||
retry := hold("late", `{"module":"late","version":"2","logs":[{"name":"l"}]}`)
|
||||
if _, err := ix.JudgeAll(ctx, map[string]bool{retry: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rows, _ := ListVerdicts(keys, get, "later", false); len(rows) != 1 {
|
||||
t.Fatalf("indexer_verdicts of later after: %+v", rows)
|
||||
}
|
||||
if got := ix.JudgeAgain(ctx, "nobody"); got.Stored || !strings.Contains(got.Answer, "holds no manifest") {
|
||||
t.Fatalf("indexer_judge of nobody: %+v", got)
|
||||
}
|
||||
if key, value, err := NewJudgeAgain("later", "why", "me", time.Now()); err != nil || key != "judge.later" {
|
||||
t.Fatalf("the ask: %q %v", key, err)
|
||||
} else if a, err := DecodeTakeIn(value); err != nil || !a.Judge || a.Module != "later" {
|
||||
t.Fatalf("the ask's value: %+v %v", a, err)
|
||||
if v := verdict(retry); v.Refused() || !strings.Contains(strings.Join(logged, "\n"), "the zero rule is not applied") {
|
||||
t.Fatalf("a count that could not be read: %+v, logged %q", v, logged)
|
||||
}
|
||||
ix.OldSections = held
|
||||
// Without the verdicts state the indexer judges nothing.
|
||||
ix.Verdicts = nil
|
||||
if j, err := ix.JudgeAll(ctx, nil); err != nil || j.Judged != 0 {
|
||||
t.Fatalf("without the state: %+v %v", j, err)
|
||||
}
|
||||
}
|
||||
|
||||
// unreadable is a store whose reads fail.
|
||||
type unreadable struct{ Store }
|
||||
|
||||
func (unreadable) Get(context.Context, string) ([]byte, uint64, bool, error) {
|
||||
return nil, 0, false, errors.New("the bus did not answer")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// The rules for bus state asked for in `requires` (issue 1966 D1; novox/hq ADR 0595 §1 to §3, ADR 0596 §2, ADR 0597 §2,
|
||||
// design 25 § 12.1). mesh-sdk keeps only the entry's shape (ADR 0578 §7); every bound below is this judge's. An entry is
|
||||
// judged once, here, and a refusal names the module, the entry and the field.
|
||||
|
||||
// optionKind is what an option's value is.
|
||||
type optionKind int
|
||||
|
||||
const (
|
||||
optionInt optionKind = iota
|
||||
optionFlag
|
||||
optionText
|
||||
optionEvents
|
||||
)
|
||||
|
||||
// optionRule is one option a template takes: its kind, its bound where it is a number, and whether it must be given.
|
||||
type optionRule struct {
|
||||
kind optionKind
|
||||
least, most int
|
||||
required bool
|
||||
}
|
||||
|
||||
// busTemplate is a template: the storage type it is on, the options it takes, and what it fixes in plain words.
|
||||
type busTemplate struct {
|
||||
storage string
|
||||
options map[string]optionRule
|
||||
fixed string
|
||||
}
|
||||
|
||||
// busTemplates are the six templates of design 25 § 12.1 and the bounds of its table.
|
||||
var busTemplates = map[string]busTemplate{
|
||||
catalogue.TemplateState: {
|
||||
storage: catalogue.StorageKeyValue,
|
||||
fixed: "what happens when full, the size of a value and of the bucket, and the copies",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionPastValues: {kind: optionInt, least: 1, most: catalogue.StateMostHistory},
|
||||
catalogue.OptionTimeToLive: {kind: optionInt, least: 1, most: 315360000},
|
||||
catalogue.OptionLifetimePerKey: {kind: optionFlag},
|
||||
catalogue.OptionPerMachine: {kind: optionFlag},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateEvents: {
|
||||
storage: catalogue.StorageStream,
|
||||
fixed: "what happens when full (the oldest is dropped) and the module's own subjects",
|
||||
options: map[string]optionRule{
|
||||
"events": {kind: optionEvents, required: true},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: catalogue.StreamLeastDays, most: catalogue.StreamMostDays},
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
catalogue.OptionGrant: {kind: optionText},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateWorkQueue: {
|
||||
storage: catalogue.StorageStream,
|
||||
fixed: "what happens when full (a new message is refused), one taker, and no grant",
|
||||
options: map[string]optionRule{
|
||||
"events": {kind: optionEvents, required: true},
|
||||
catalogue.OptionDeliveries: {kind: optionInt, least: catalogue.StreamLeastDeliveries, most: catalogue.StreamMostDeliveries},
|
||||
catalogue.OptionDuplicates: {kind: optionInt, least: 1, most: catalogue.StreamMostDays * 86400},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: catalogue.StreamLeastDays, most: catalogue.StreamMostDays},
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateLog: {
|
||||
storage: catalogue.StorageStream,
|
||||
fixed: "what happens when full (a new entry is refused), that an entry is never rewritten, and its size",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateObjects: {
|
||||
storage: catalogue.StorageObjectStore,
|
||||
fixed: "what happens when full (a new object is refused) and that objects are kept for good",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.ObjectsLeastMiB, most: catalogue.ObjectsMostMiB, required: true},
|
||||
catalogue.OptionGrant: {kind: optionText},
|
||||
},
|
||||
},
|
||||
catalogue.TemplateLedger: {
|
||||
storage: catalogue.StorageLedger,
|
||||
fixed: "what happens when full (a new entry is refused) and its state and index buckets",
|
||||
options: map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// bareStream is a stream of a module's own configuration, where no template fits: the three settings the judge
|
||||
// refuses one for missing (design 25 § 12.1), and those it may also give.
|
||||
var bareStream = map[string]optionRule{
|
||||
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB, required: true},
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650, required: true},
|
||||
catalogue.OptionGrant: {kind: optionText},
|
||||
"events": {kind: optionEvents},
|
||||
}
|
||||
|
||||
// ledgerConfiguration is what a ledger's own configuration may give: how long its history is kept (design 25 § 12.1).
|
||||
var ledgerConfiguration = map[string]optionRule{
|
||||
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650},
|
||||
}
|
||||
|
||||
// noAge is how a bare stream says it keeps its messages for good.
|
||||
const noAge = "none"
|
||||
|
||||
// ledgerEffects are the four effects an operation may have (ADR 0597 §2).
|
||||
var ledgerEffects = []string{catalogue.EffectSetFields, catalogue.EffectSetEntry, catalogue.EffectAppend, catalogue.EffectRemove}
|
||||
|
||||
// BusStateProblems is what is wrong with one manifest's bus state, judged on the manifest alone (issue 1966 D1).
|
||||
func BusStateProblems(m catalogue.Manifest) []string {
|
||||
if len(m.BusState) == 0 {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
if !stateName.MatchString(m.Module) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s keeps bus state, and a module's name is part of its names on the bus, which take one plain name — "+
|
||||
"no dot (novox/hq ADR 0201, ADR 0595 §1)", m.Module))
|
||||
}
|
||||
storageOf := map[string]string{}
|
||||
oldSection := map[string]string{} // a local name an old section keeps, to that section (issue 1966 D2)
|
||||
for _, st := range m.State {
|
||||
oldSection[st.Name] = "state"
|
||||
}
|
||||
for _, l := range m.Logs {
|
||||
oldSection[l.Name] = "logs"
|
||||
}
|
||||
for _, st := range m.Streams {
|
||||
oldSection[st.Name] = "streams"
|
||||
}
|
||||
for _, o := range m.Objects {
|
||||
oldSection[o.Name] = "objects"
|
||||
}
|
||||
terminal := catalogue.TerminalKeys(m)
|
||||
for _, e := range m.BusState {
|
||||
at := fmt.Sprintf("%s's bus state %q", m.Module, e.Name)
|
||||
switch {
|
||||
case strings.Contains(e.Name, "_"):
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: its name holds an underscore, which separates the module from the local name on the bus; use a "+
|
||||
"hyphen (novox/hq design 25 § 12.1)", at))
|
||||
case !stateName.MatchString(e.Name):
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: its name is a local name — lower-case letters, digits and hyphens, no dot and no underscore; the "+
|
||||
"mesh derives the name on the bus (novox/hq ADR 0201)", at))
|
||||
}
|
||||
if first, taken := storageOf[e.Name]; taken {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the name is used twice (as %s and as %s); each name is one, whatever its storage type (novox/hq "+
|
||||
"ADR 0595 §1)", at, first, e.Storage))
|
||||
}
|
||||
storageOf[e.Name] = e.Storage
|
||||
if section, kept := oldSection[e.Name]; kept {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the name is also kept in the old section %s; one local name is one form, so a module that moves an "+
|
||||
"entry removes it from the old section (novox/hq ADR 0595 §11)", at, section))
|
||||
}
|
||||
problems = append(problems, busEntryProblems(at, e, terminal)...)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// busEntryProblems judges one entry's storage type, template, options and configuration.
|
||||
func busEntryProblems(at string, e catalogue.BusStateEntry, terminal []string) []string {
|
||||
var problems []string
|
||||
switch e.Storage {
|
||||
case catalogue.StorageStream, catalogue.StorageKeyValue, catalogue.StorageObjectStore, catalogue.StorageLedger:
|
||||
default:
|
||||
return []string{fmt.Sprintf("%s: the storage type %q is unknown; it is %s", at, e.Storage,
|
||||
strings.Join([]string{catalogue.StorageStream, catalogue.StorageKeyValue, catalogue.StorageObjectStore, catalogue.StorageLedger}, ", "))}
|
||||
}
|
||||
if e.Storage != catalogue.StorageLedger && len(e.Types) > 0 {
|
||||
problems = append(problems, fmt.Sprintf("%s: types are a ledger's; storage type %s has none", at, e.Storage))
|
||||
}
|
||||
if e.Template == "" {
|
||||
if e.Storage != catalogue.StorageStream {
|
||||
return append(problems, fmt.Sprintf(
|
||||
"%s: the storage type %s is asked for by its template; only a stream is asked for bare, with a "+
|
||||
"configuration of its own (novox/hq design 25 § 12.1)", at, e.Storage))
|
||||
}
|
||||
if len(e.Options) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: a bare stream gives its settings in its configuration, not in options", at))
|
||||
}
|
||||
return append(problems, settingsProblems(at, "configuration", e.Configuration, bareStream, "a bare stream", terminal, true)...)
|
||||
}
|
||||
tpl, known := busTemplates[e.Template]
|
||||
switch {
|
||||
case !known:
|
||||
names := make([]string, 0, len(busTemplates))
|
||||
for n := range busTemplates {
|
||||
names = append(names, n)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return append(problems, fmt.Sprintf("%s: the template %q is unknown; it is %s", at, e.Template, strings.Join(names, ", ")))
|
||||
case tpl.storage != e.Storage:
|
||||
return append(problems, fmt.Sprintf(
|
||||
"%s: the template %s is on the storage type %s, not %s", at, e.Template, tpl.storage, e.Storage))
|
||||
}
|
||||
problems = append(problems, settingsProblems(at, "options", e.Options, tpl.options, "the template "+e.Template+" (it fixes "+tpl.fixed+")", terminal, false)...)
|
||||
if e.Template == catalogue.TemplateLedger {
|
||||
problems = append(problems, settingsProblems(at, "configuration", e.Configuration, ledgerConfiguration, "a ledger's own configuration", terminal, false)...)
|
||||
problems = append(problems, ledgerTypeProblems(at, e)...)
|
||||
} else if len(e.Configuration) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the template %s is given a configuration; a configuration of its own is for a bare stream or a "+
|
||||
"ledger whose history expires, and a template's settings are its options (novox/hq ADR 0595 §2)", at, e.Template))
|
||||
}
|
||||
if e.Template == catalogue.TemplateWorkQueue {
|
||||
problems = append(problems, workQueueProblems(at, e)...)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// settingsProblems judges the given settings against the rules: an unknown one is refused (a fixed setting among them),
|
||||
// a number outside its bound, a kind mismatch, and a required one missing. A bare stream's when-full and age are read
|
||||
// here too.
|
||||
func settingsProblems(at, field string, given map[string]json.RawMessage, rules map[string]optionRule, of string, terminal []string, bare bool) []string {
|
||||
var problems []string
|
||||
for _, k := range sortedKeys(given) {
|
||||
raw := given[k]
|
||||
if bare && k == catalogue.ConfigWhenFull {
|
||||
var v string
|
||||
if json.Unmarshal(raw, &v) != nil || (v != catalogue.WhenFullRefuse && v != catalogue.WhenFullDropOldest) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is %s; a bare stream says %q or %q", at, field, k, string(raw), catalogue.WhenFullRefuse, catalogue.WhenFullDropOldest))
|
||||
}
|
||||
continue
|
||||
}
|
||||
rule, ok := rules[k]
|
||||
if !ok {
|
||||
if k == catalogue.ConfigWhenFull {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is given to %s; what happens when full is fixed and is never an option (novox/hq ADR 0595 §3)", at, field, k, of))
|
||||
continue
|
||||
}
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is not a setting of %s (novox/hq ADR 0595 §3)", at, field, k, of))
|
||||
continue
|
||||
}
|
||||
problems = append(problems, valueProblems(at, field, k, raw, rule, terminal, bare)...)
|
||||
}
|
||||
for _, k := range sortedKeys(rules) {
|
||||
if _, given := given[k]; !given && rules[k].required {
|
||||
problems = append(problems, fmt.Sprintf("%s: %s %q is missing; %s gives it", at, field, k, of))
|
||||
}
|
||||
}
|
||||
if bare {
|
||||
if _, given := given[catalogue.ConfigWhenFull]; !given {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %s %q is missing; a bare stream says %q or %q", at, field, catalogue.ConfigWhenFull, catalogue.WhenFullRefuse, catalogue.WhenFullDropOldest))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// valueProblems judges one setting's value against its rule.
|
||||
func valueProblems(at, field, key string, raw json.RawMessage, rule optionRule, terminal []string, bare bool) []string {
|
||||
switch rule.kind {
|
||||
case optionInt:
|
||||
if bare && key == catalogue.OptionAgeDays {
|
||||
var word string
|
||||
if json.Unmarshal(raw, &word) == nil && word == noAge {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
n, ok := wholeNumber(raw)
|
||||
if !ok {
|
||||
return []string{fmt.Sprintf("%s: %s %q is %s, which is not a whole number", at, field, key, string(raw))}
|
||||
}
|
||||
if n < rule.least || n > rule.most {
|
||||
extra := ""
|
||||
if key == catalogue.OptionPastValues && n == 0 {
|
||||
extra = "; 0 is refused here"
|
||||
}
|
||||
return []string{fmt.Sprintf("%s: %s %q is %d, outside its bound of %d to %d%s", at, field, key, n, rule.least, rule.most, extra)}
|
||||
}
|
||||
case optionFlag:
|
||||
var b bool
|
||||
if json.Unmarshal(raw, &b) != nil {
|
||||
return []string{fmt.Sprintf("%s: %s %q is %s, which is not true or false", at, field, key, string(raw))}
|
||||
}
|
||||
case optionText:
|
||||
var config string
|
||||
if json.Unmarshal(raw, &config) != nil || !slices.Contains(terminal, config) {
|
||||
return []string{fmt.Sprintf(
|
||||
"%s: %s %q names %s, which is not a trusted config value of the module, set only at the controller's "+
|
||||
"terminal or on the operator's Approve (novox/hq ADR 0348 §8)", at, field, key, string(raw))}
|
||||
}
|
||||
case optionEvents:
|
||||
var events []string
|
||||
if json.Unmarshal(raw, &events) != nil || len(events) == 0 {
|
||||
return []string{fmt.Sprintf("%s: %s %q is %s; it is a list of at least one event", at, field, key, string(raw))}
|
||||
}
|
||||
var out []string
|
||||
for _, ev := range events {
|
||||
if err := eventPattern(ev, false); err != nil {
|
||||
out = append(out, fmt.Sprintf("%s: %s %q carries %q: %v", at, field, key, ev, err))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wholeNumber reads a JSON number that is a whole number.
|
||||
func wholeNumber(raw json.RawMessage) (int, bool) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
var n json.Number
|
||||
if dec.Decode(&n) != nil {
|
||||
return 0, false
|
||||
}
|
||||
i, err := n.Int64()
|
||||
return int(i), err == nil
|
||||
}
|
||||
|
||||
// workQueueProblems holds a work queue's duplicate window to its age (design 25 § 12.1: up to its age).
|
||||
func workQueueProblems(at string, e catalogue.BusStateEntry) []string {
|
||||
raw, given := e.Options[catalogue.OptionDuplicates]
|
||||
if !given {
|
||||
return nil
|
||||
}
|
||||
window, ok := wholeNumber(raw)
|
||||
if !ok {
|
||||
return nil // valueProblems said so
|
||||
}
|
||||
days := catalogue.StreamDefaultDays
|
||||
if age, ok := wholeNumber(e.Options[catalogue.OptionAgeDays]); ok && age > 0 {
|
||||
days = age
|
||||
}
|
||||
if window > days*86400 {
|
||||
return []string{fmt.Sprintf(
|
||||
"%s: options %q is %d seconds, longer than the queue's age of %d days (novox/hq design 25 § 12.1)",
|
||||
at, catalogue.OptionDuplicates, window, days)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ledgerTypeProblems judges a ledger's types: named locally and once, and every operation's effect one of the four.
|
||||
func ledgerTypeProblems(at string, e catalogue.BusStateEntry) []string {
|
||||
var problems []string
|
||||
if len(e.Types) == 0 {
|
||||
problems = append(problems, fmt.Sprintf("%s: a ledger names at least one type (novox/hq ADR 0597 §1)", at))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, t := range e.Types {
|
||||
where := fmt.Sprintf("%s: type %q", at, t.Name)
|
||||
switch {
|
||||
case !stateName.MatchString(t.Name):
|
||||
problems = append(problems, fmt.Sprintf("%s: its name is lower-case letters, digits and hyphens", where))
|
||||
case seen[t.Name]:
|
||||
problems = append(problems, fmt.Sprintf("%s: the type is named twice", where))
|
||||
}
|
||||
seen[t.Name] = true
|
||||
operations := map[string]bool{}
|
||||
for _, op := range t.Operations {
|
||||
if operations[op.Name] {
|
||||
problems = append(problems, fmt.Sprintf("%s: the operation %q is named twice", where, op.Name))
|
||||
}
|
||||
operations[op.Name] = true
|
||||
if !slices.Contains(ledgerEffects, op.Effect) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: the operation %q has the effect %q, outside the four (%s) (novox/hq ADR 0597 §2)",
|
||||
where, op.Name, op.Effect, strings.Join(ledgerEffects, ", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// BusStateCatalogueProblems are the rules of bus state no single manifest can be judged against: two modules' names on
|
||||
// the bus alike, and a `reads` naming bus state in the new form, which has no readers (novox/hq ADR 0596 §2).
|
||||
func BusStateCatalogueProblems(shelf catalogue.Shelf) []string {
|
||||
var problems []string
|
||||
owner := map[string]string{} // a name on the bus, upper-cased, to the module asking for it
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
for _, o := range shelf[module].Objects {
|
||||
owner[strings.ToUpper(o.Bucket(module))] = module
|
||||
}
|
||||
}
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
for _, e := range shelf[module].BusState {
|
||||
name := catalogue.ObjectsBucket(module, e.Name)
|
||||
if first, taken := owner[strings.ToUpper(name)]; taken && first != module {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's bus state %q has the name %s on the bus, which %s's already has or is alike; a name on the bus "+
|
||||
"has one owner (novox/hq ADR 0201 §4)", module, e.Name, name, first))
|
||||
continue
|
||||
}
|
||||
owner[strings.ToUpper(name)] = module
|
||||
}
|
||||
}
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
for _, r := range shelf[module].Reads {
|
||||
from, local, err := catalogue.ReadState(r)
|
||||
if err != nil || from == module {
|
||||
continue // StateProblems says why
|
||||
}
|
||||
for _, e := range shelf[from].BusState {
|
||||
if e.Name == local {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s reads %q, which %s has moved to bus state in `requires`; the new form has no readers: ask %s's "+
|
||||
"tools, or hear its events (novox/hq ADR 0596 §2)", module, r, from, from))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// A manifest's bus state, asked for in `requires` by template, is judged once and each refusal names the module, the entry
|
||||
// and the field (issue 1966 D1, D2; novox/hq design 25 § 12.1, ADR 0595, ADR 0596 §2, ADR 0597 §2).
|
||||
func TestBusStateIsJudgedOnce(t *testing.T) {
|
||||
const ledger = `{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"types":[` +
|
||||
`{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}`
|
||||
manifest := func(entries string) string {
|
||||
return `{"module":"a","version":"1","requires":[` + entries + `]}`
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
raw string
|
||||
refused []string // words the first problem holds, in order: absent is accepted
|
||||
}{
|
||||
// 1966/AC8: a state, a log, objects and a ledger within bounds
|
||||
{"within bounds", manifest(`{"name":"cache","storage":"key-value","template":"state","options":{"history":8,"key-ttl":true}},` +
|
||||
`{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64,"max-age-days":365}},` +
|
||||
`{"name":"files","storage":"object-store","template":"objects","options":{"max-mib":2048}},` + ledger), nil},
|
||||
// 1966/AC7: each refused case, naming the module, the entry and the field
|
||||
{"unknown storage type", manifest(`{"name":"x","storage":"tape"}`), []string{"a's bus state \"x\"", "storage type \"tape\""}},
|
||||
{"unknown template", manifest(`{"name":"x","storage":"stream","template":"pile"}`), []string{"\"x\"", "template \"pile\""}},
|
||||
{"template on another storage type", manifest(`{"name":"x","storage":"stream","template":"state"}`), []string{"\"x\"", "template state is on the storage type key-value"}},
|
||||
{"past values 0", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":0}}`), []string{"\"x\"", "options \"history\" is 0", "0 is refused"}},
|
||||
{"past values 65", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":65}}`), []string{"\"x\"", "\"history\" is 65", "1 to 64"}},
|
||||
{"a lifetime that is no number", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"ttl-seconds":"soon"}}`), []string{"\"x\"", "\"ttl-seconds\""}},
|
||||
{"events age over 30 days", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-age-days":31}}`), []string{"\"x\"", "\"max-age-days\" is 31", "1 to 30"}},
|
||||
{"events cap over 8192", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-mib":8193}}`), []string{"\"x\"", "\"max-mib\" is 8193"}},
|
||||
{"events naming none", manifest(`{"name":"x","storage":"stream","template":"events","options":{}}`), []string{"\"x\"", "\"events\" is missing"}},
|
||||
{"a grant that is no trusted value", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"grant":"nobody"}}`), []string{"\"x\"", "\"grant\"", "trusted config value"}},
|
||||
{"deliveries 11", manifest(`{"name":"x","storage":"stream","template":"work-queue","options":{"events":["job"],"max-deliveries":11}}`), []string{"\"x\"", "\"max-deliveries\" is 11", "1 to 10"}},
|
||||
{"duplicates past the age", manifest(`{"name":"x","storage":"stream","template":"work-queue","options":{"events":["job"],"max-age-days":1,"duplicates":86401}}`), []string{"\"x\"", "\"duplicates\"", "longer than the queue's age of 1 days"}},
|
||||
{"log age 3651", manifest(`{"name":"x","storage":"stream","template":"log","options":{"max-age-days":3651}}`), []string{"\"x\"", "\"max-age-days\" is 3651"}},
|
||||
{"objects without a cap", manifest(`{"name":"x","storage":"object-store","template":"objects"}`), []string{"\"x\"", "\"max-mib\" is missing"}},
|
||||
{"when full given as an option", manifest(`{"name":"x","storage":"stream","template":"log","options":{"when-full":"drop-oldest"}}`), []string{"\"x\"", "\"when-full\"", "fixed"}},
|
||||
{"another fixed setting given", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"max-value-kib":1024}}`), []string{"\"x\"", "\"max-value-kib\" is not a setting of the template state"}},
|
||||
{"a configuration beside a template", manifest(`{"name":"x","storage":"key-value","template":"state","configuration":{"max-mib":1}}`), []string{"\"x\"", "configuration"}},
|
||||
{"a name used twice across storage types", manifest(
|
||||
`{"name":"x","storage":"key-value","template":"state"},{"name":"x","storage":"stream","template":"log"}`), []string{"\"x\"", "used twice (as key-value and as stream)"}},
|
||||
{"a local name holding an underscore", manifest(`{"name":"my_x","storage":"key-value","template":"state"}`), []string{"\"my_x\"", "underscore"}},
|
||||
{"a bare stream without when full", manifest(`{"name":"x","storage":"stream","configuration":{"max-mib":10,"max-age-days":"none"}}`), []string{"\"x\"", "\"when-full\" is missing"}},
|
||||
{"a bare stream without a cap", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"refuse","max-age-days":3}}`), []string{"\"x\"", "\"max-mib\" is missing"}},
|
||||
{"a bare stream without an age", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"refuse","max-mib":10}}`), []string{"\"x\"", "\"max-age-days\" is missing"}},
|
||||
{"a bare stream, complete", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"drop-oldest","max-mib":10,"max-age-days":"none"}}`), nil},
|
||||
{"a bare stream with a wrong when full", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"crash","max-mib":10,"max-age-days":9}}`), []string{"\"x\"", "\"when-full\"", "\"refuse\" or \"drop-oldest\""}},
|
||||
{"a ledger effect outside the four", manifest(strings.Replace(ledger, `"set-fields"`, `"upsert"`, 1)), []string{"\"records\"", "effect \"upsert\"", "outside the four"}},
|
||||
{"a ledger whose history expires", manifest(strings.Replace(ledger, `"options"`, `"configuration":{"max-age-days":30},"options"`, 1)), nil},
|
||||
} {
|
||||
_, problems := Manifest([]byte(c.raw), nil)
|
||||
switch {
|
||||
case c.refused == nil && len(problems) > 0:
|
||||
t.Errorf("%s: refused: %v", c.name, problems)
|
||||
case c.refused != nil && len(problems) == 0:
|
||||
t.Errorf("%s: accepted", c.name)
|
||||
case c.refused != nil:
|
||||
text := strings.Join(problems, "\n")
|
||||
at := 0
|
||||
for _, word := range c.refused {
|
||||
i := strings.Index(text[at:], word)
|
||||
if i < 0 {
|
||||
t.Errorf("%s: problems %q lack %q", c.name, text, word)
|
||||
break
|
||||
}
|
||||
at += i
|
||||
}
|
||||
}
|
||||
}
|
||||
// Between the manifests: two modules' names on the bus alike, and a read of bus state that moved (ADR 0596 §2).
|
||||
entry := func(name string) []catalogue.BusStateEntry {
|
||||
return []catalogue.BusStateEntry{{Name: name, Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState}}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
shelf catalogue.Shelf
|
||||
want string
|
||||
}{
|
||||
{"names alike", catalogue.Shelf{"a_b": {Module: "a_b", BusState: entry("c")}, "a": {Module: "a", BusState: entry("b_c")}}, "has one owner"},
|
||||
{"a read of moved state", catalogue.Shelf{"owner": {Module: "owner", BusState: entry("cache")},
|
||||
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, "reader reads \"owner.cache\", which owner has moved"},
|
||||
{"a read of state still kept", catalogue.Shelf{"owner": {Module: "owner", State: []catalogue.StateDeclaration{{Name: "cache"}}},
|
||||
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, ""},
|
||||
} {
|
||||
got := strings.Join(BusStateCatalogueProblems(c.shelf), "\n")
|
||||
if (c.want == "") != (got == "") || !strings.Contains(got, c.want) {
|
||||
t.Errorf("%s: between the manifests: %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,27 +69,15 @@ func problemsOf(err error) []string {
|
||||
// whose removal from the catalogue ends it (a rule two modules break together refuses both); one no removal ends is in
|
||||
// unnamed, for the caller to say. A module not in the answer is accepted.
|
||||
func Shelf(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int) (refused map[string][]string, unnamed []string) {
|
||||
refused, waits, unnamed := ShelfWaiting(raws, predefined, longestMachine)
|
||||
for name := range waits {
|
||||
// Strictly, as the checker judges a change: the judge that runs there is the one built at the change's head, which
|
||||
// knows the fields the change brings, so a field it does not know is a mistake.
|
||||
_, refused[name] = Manifest(raws[name], predefined)
|
||||
}
|
||||
return refused, unnamed
|
||||
return ShelfCounting(raws, predefined, longestMachine, nil)
|
||||
}
|
||||
|
||||
// ShelfWaiting is Shelf where a manifest that carries a field this judge does not know (UnknownFields) is not refused: it is
|
||||
// in waits, by module, naming each field, and judged no further, because a newer judge may know the field (novox/hq ADR
|
||||
// 0578 §1, amended). The indexer judges so; the checker, which runs the judge built at the head of the change, judges with
|
||||
// Shelf.
|
||||
func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int) (refused, waits map[string][]string, unnamed []string) {
|
||||
refused, waits = map[string][]string{}, map[string][]string{}
|
||||
// ShelfCounting is Shelf also refusing a manifest of a module outside the foundation that uses an old section whose count
|
||||
// is 0 (OldSectionProblems; issue 1966 D4); nil counts refuse none.
|
||||
func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts) (refused map[string][]string, unnamed []string) {
|
||||
refused = map[string][]string{}
|
||||
shelf := catalogue.Shelf{}
|
||||
for name, raw := range raws {
|
||||
if unknown := UnknownFields(raw); len(unknown) > 0 {
|
||||
waits[name] = unknown
|
||||
continue
|
||||
}
|
||||
m, problems := Manifest(raw, predefined)
|
||||
if len(problems) > 0 {
|
||||
refused[name] = problems
|
||||
@@ -99,6 +87,10 @@ func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, l
|
||||
refused[name] = []string{fmt.Sprintf("the manifest is of module %q, not %q", m.Module, name)}
|
||||
continue
|
||||
}
|
||||
if problems := OldSectionProblems(m, counts); len(problems) > 0 {
|
||||
refused[name] = problems
|
||||
continue
|
||||
}
|
||||
shelf[name] = m
|
||||
}
|
||||
between := Between(shelf, longestMachine)
|
||||
@@ -140,7 +132,7 @@ func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, l
|
||||
for name := range refused {
|
||||
sort.Strings(refused[name])
|
||||
}
|
||||
return refused, waits, unnamed
|
||||
return refused, unnamed
|
||||
}
|
||||
|
||||
// Between is every problem between the manifests of shelf, sorted: catalogue.CatalogueProblems, IdentityProblems over
|
||||
@@ -197,6 +189,7 @@ const Unnamed = "(between manifests)"
|
||||
func Request(r composer.Request) (composer.Verdict, error) {
|
||||
v := composer.Verdict{V: composer.Version, SDK: Identity()}
|
||||
seen := map[string]bool{}
|
||||
counts := trunkOldSections(r)
|
||||
for _, c := range r.Cases {
|
||||
if seen[c.Name] {
|
||||
return composer.Verdict{}, fmt.Errorf("the case %q is named twice", c.Name)
|
||||
@@ -209,7 +202,7 @@ func Request(r composer.Request) (composer.Verdict, error) {
|
||||
}
|
||||
raws[name] = m.Raw
|
||||
}
|
||||
refused, unnamed := Shelf(raws, r.Predefined, 0)
|
||||
refused, unnamed := ShelfCounting(raws, r.Predefined, 0, counts)
|
||||
cv := composer.CaseVerdict{Name: c.Name, Manifests: map[string]composer.ManifestRead{}}
|
||||
for name := range raws {
|
||||
if problems := refused[name]; len(problems) > 0 {
|
||||
@@ -226,6 +219,39 @@ func Request(r composer.Request) (composer.Verdict, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// TrunkCase is the case of a request holding the catalogue as the trunk holds it (the checker's JudgeTrunk).
|
||||
const TrunkCase = "trunk"
|
||||
|
||||
// trunkOldSections counts the old sections the modules accepted at the trunk use, as the indexer counts them from its
|
||||
// verdicts (issue 1966 D4, ADR 0595 §11): a change is refused at the merge check exactly as at take-in (judged once, ADR
|
||||
// 0578). Nil where the request holds no trunk case, which refuses no use of a section.
|
||||
func trunkOldSections(r composer.Request) OldSectionCounts {
|
||||
for _, c := range r.Cases {
|
||||
if c.Name != TrunkCase {
|
||||
continue
|
||||
}
|
||||
raws := map[string][]byte{}
|
||||
for name, m := range c.Manifests {
|
||||
if len(m.Raw) > 0 {
|
||||
raws[name] = m.Raw
|
||||
}
|
||||
}
|
||||
refused, _ := Shelf(raws, r.Predefined, 0)
|
||||
var accepted []catalogue.Manifest
|
||||
for name, raw := range raws {
|
||||
if len(refused[name]) > 0 {
|
||||
continue
|
||||
}
|
||||
if m, err := catalogue.Decode(raw); err == nil {
|
||||
m.Module = name
|
||||
accepted = append(accepted, m)
|
||||
}
|
||||
}
|
||||
return CountOldSections(accepted)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
identityOnce sync.Once
|
||||
identity string
|
||||
|
||||
@@ -54,21 +54,6 @@ func TestTheJudge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A field the judge does not know waits in the indexer's judge and stays a refusal in the checker's (clause 3 of the
|
||||
// record on a release's Approve); a renamed field and any other problem are refused by both.
|
||||
waitRaws := map[string][]byte{
|
||||
"later": []byte(`{"module":"later","version":"1","core-credentials":true,"other":1}`),
|
||||
"renamed": []byte(`{"module":"renamed","version":"1","needs":{"x":"y"}}`),
|
||||
"bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`),
|
||||
"ok": []byte(`{"module":"ok","version":"1"}`),
|
||||
}
|
||||
refused, waits, _ := ShelfWaiting(waitRaws, nil, 0)
|
||||
if !reflect.DeepEqual(waits, map[string][]string{"later": {"core-credentials", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil {
|
||||
t.Errorf("ShelfWaiting: waits %v, refused %v", waits, refused)
|
||||
}
|
||||
if strict, _ := Shelf(waitRaws, nil, 0); len(strict) != 3 || !strings.Contains(strings.Join(strict["later"], ""), "core-credentials") {
|
||||
t.Errorf("Shelf refuses as before: %v", strict)
|
||||
}
|
||||
// The private network's manifest, which the mesh provides itself, is accepted (moved from mesh-sdk overlay's test).
|
||||
if raw, err := json.Marshal(overlay.Manifest()); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -101,6 +86,27 @@ func TestTheJudge(t *testing.T) {
|
||||
v.Cases[0].Manifests["b"].Accepted || !strings.Contains(v.Cases[0].Manifests["b"].Refused, `requires "amqp"`) {
|
||||
t.Fatalf("request: %+v %v", v, err)
|
||||
}
|
||||
// 1966/AC11: the checker's judge counts the old sections from the trunk case as the indexer does from its verdicts (the
|
||||
// foundation left out): a change adding logs where no trunk module uses them is refused, the foundation's accepted, and
|
||||
// state, which a trunk module uses, is accepted.
|
||||
old := composer.Request{V: composer.Version, Cases: []composer.Case{
|
||||
{Name: "change", Manifests: map[string]composer.Manifest{
|
||||
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)},
|
||||
"late": {Raw: json.RawMessage(`{"module":"late","version":"1","logs":[{"name":"l"}]}`)},
|
||||
"newer": {Raw: json.RawMessage(`{"module":"newer","version":"1","state":["y"]}`)},
|
||||
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}},
|
||||
{Name: "trunk", Manifests: map[string]composer.Manifest{
|
||||
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)},
|
||||
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}}}
|
||||
ov, err := Request(old)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := ov.Cases[0].Manifests
|
||||
if got["late"].Accepted || !strings.Contains(got["late"].Refused, "late uses the old section logs") ||
|
||||
!got["newer"].Accepted || !got["user"].Accepted || !got["mesh-grants"].Accepted {
|
||||
t.Errorf("the change's old sections: %+v", got)
|
||||
}
|
||||
r.Cases[0].Manifests["c"] = composer.Manifest{Digest: strings.Repeat("a", 64)}
|
||||
if _, err := Request(r); err == nil {
|
||||
t.Fatal("a manifest left as a digest was judged")
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// The old sections end when no module is left in them (issue 1966 D4; novox/hq ADR 0595 §11): the indexer counts, per old
|
||||
// section, the accepted modules using it, the foundation left out, and the judge refuses a manifest of another module
|
||||
// that uses a section whose count is 0. The foundation (the bus module, the grants part and the indexer itself) is
|
||||
// created before the bus module can create anything, so it keeps the old sections for good and is always accepted.
|
||||
|
||||
// OldSections are the sections of a manifest that keep bus state in the old forms.
|
||||
var OldSections = []string{"state", "logs", "streams", "objects"}
|
||||
|
||||
// Foundation are the modules that keep using the old sections for good.
|
||||
var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer"}
|
||||
|
||||
// OldSectionCounts is, per old section, how many accepted modules outside the foundation use it. Nil is no count: the
|
||||
// judge then refuses no use of a section.
|
||||
type OldSectionCounts map[string]int
|
||||
|
||||
// SectionsUsed are the old sections a manifest uses, in OldSections' order.
|
||||
func SectionsUsed(m catalogue.Manifest) []string {
|
||||
var used []string
|
||||
for _, s := range OldSections {
|
||||
if (s == "state" && len(m.State) > 0) || (s == "logs" && len(m.Logs) > 0) ||
|
||||
(s == "streams" && len(m.Streams) > 0) || (s == "objects" && len(m.Objects) > 0) {
|
||||
used = append(used, s)
|
||||
}
|
||||
}
|
||||
return used
|
||||
}
|
||||
|
||||
// CountOldSections counts, per old section, the manifests using it, the foundation's left out; every section is in the
|
||||
// answer, with 0 where none uses it.
|
||||
func CountOldSections(manifests []catalogue.Manifest) OldSectionCounts {
|
||||
counts := OldSectionCounts{}
|
||||
for _, s := range OldSections {
|
||||
counts[s] = 0
|
||||
}
|
||||
for _, m := range manifests {
|
||||
if slices.Contains(Foundation, m.Module) {
|
||||
continue
|
||||
}
|
||||
for _, s := range SectionsUsed(m) {
|
||||
counts[s]++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
// OldSectionProblems are the old sections a manifest uses that no accepted module outside the foundation uses any more.
|
||||
func OldSectionProblems(m catalogue.Manifest, counts OldSectionCounts) []string {
|
||||
if counts == nil || slices.Contains(Foundation, m.Module) {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
for _, s := range SectionsUsed(m) {
|
||||
if counts[s] == 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s uses the old section %s, which no other module uses any more (0 modules count); ask for it in "+
|
||||
"`requires` by template (novox/hq ADR 0595 §11)", m.Module, s))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
@@ -2388,44 +2388,6 @@ func ownSecretPromptProblems(m catalogue.Manifest, name, prompt string, family b
|
||||
return problems
|
||||
}
|
||||
|
||||
// unknownFieldText is how the lenient decoder words the first field its shape does not have, and readWithout the list of
|
||||
// every field it read the manifest without (catalogue.Manifest.SetAside).
|
||||
var (
|
||||
unknownFieldText = regexp.MustCompile(`json: unknown field "([^"]+)"`)
|
||||
readWithout = regexp.MustCompile(`\(read without ([^)]*)\)`)
|
||||
)
|
||||
|
||||
// UnknownFields are the fields of a manifest the judge does not know, as the lenient decoder sets them aside (catalogue.Decode,
|
||||
// each once, the path it names them with), or none: a manifest this judge reads whole has none, and a field that used to
|
||||
// mean something (renamed) is a known mistake, not a field a later judge may know, so a manifest with one has none
|
||||
// either.
|
||||
func UnknownFields(raw []byte) []string {
|
||||
m, err := catalogue.Decode(raw)
|
||||
aside := m.SetAside()
|
||||
if err != nil || aside == "" {
|
||||
return nil
|
||||
}
|
||||
first := unknownFieldText.FindStringSubmatch(aside)
|
||||
if first == nil {
|
||||
return nil // set aside for another reason: the judge's refusal as it is
|
||||
}
|
||||
fields := []string{first[1]}
|
||||
if list := readWithout.FindStringSubmatch(aside); list != nil {
|
||||
fields = fields[:0]
|
||||
for _, f := range strings.Split(list[1], ", ") {
|
||||
if f = strings.TrimSpace(f); f != "" && !slices.Contains(fields, f) {
|
||||
fields = append(fields, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range fields {
|
||||
if _, was := renamed[f[strings.LastIndex(f, ".")+1:]]; was {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.Manifest, error) {
|
||||
var m catalogue.Manifest
|
||||
// Strictly. **An unknown key is refused**, which is the discipline the host's declaration
|
||||
@@ -2664,6 +2626,8 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
|
||||
// And the object buckets it owns (novox/hq ADR 0592 §5, §9).
|
||||
problems = append(problems, ObjectsProblems(m)...)
|
||||
problems = append(problems, WorkQueueStreamProblems(m)...)
|
||||
// And the bus state it asks for in `requires`, by template (busstate.go, novox/hq ADR 0595; issue 1966 D1).
|
||||
problems = append(problems, BusStateProblems(m)...)
|
||||
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262), or, written in the
|
||||
// config form, the whole declaration of each value (config.go, novox/hq ADR 0348).
|
||||
if m.DeclaresConfig() {
|
||||
@@ -4592,6 +4556,8 @@ func CatalogueProblems(shelf catalogue.Shelf) []string {
|
||||
problems = append(problems, StreamNameProblems(shelf)...)
|
||||
// An object bucket's name has one owner, and another module reads it only through its owner's grant (ADR 0592 §5, §9).
|
||||
problems = append(problems, ObjectsCatalogueProblems(shelf)...)
|
||||
// Bus state in `requires`: a name on the bus has one owner, and the new form has no readers (ADR 0595, ADR 0596 §2).
|
||||
problems = append(problems, BusStateCatalogueProblems(shelf)...)
|
||||
// A module that hears a restricted stream runs as an account of its own (novox/hq ADR 0376 §2).
|
||||
problems = append(problems, RestrictedHearingProblems(shelf)...)
|
||||
// A work-queue stream is never asked for by another module's consumes (novox/hq ADR 0436 §6).
|
||||
@@ -5396,7 +5362,7 @@ func StateProblems(m catalogue.Manifest) []string {
|
||||
seen[s.Name] = true
|
||||
if s.History < 0 || s.History > catalogue.StateMostHistory {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s keeps %d values of %q; a key keeps between 1 and %d", m.Module, s.History, s.Name, catalogue.StateMostHistory))
|
||||
"%s keeps %d values of %q; a key keeps 0 or 1 to %d (0 is taken as 1)", m.Module, s.History, s.Name, catalogue.StateMostHistory))
|
||||
}
|
||||
if s.TTLSeconds < 0 {
|
||||
problems = append(problems, fmt.Sprintf("%s gives %q a negative lifetime", m.Module, s.Name))
|
||||
|
||||
@@ -54,12 +54,6 @@ func TestABinaryMustSayWhichSystemItIsFor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestABinaryThatNamesASystemIsAccepted(t *testing.T) {
|
||||
if got := buildProblemsOf(t, bundleFor("go", "arch")); got != "" {
|
||||
t.Fatalf("a compiled bundle naming a system was refused:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABlockOutsideTheShapeIsRefused(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`{"seat":"node-bar","kind":"block","content":"[[block]]"}`: "has no data",
|
||||
@@ -913,6 +907,12 @@ func TestALogIsNamedLocallyAndCapped(t *testing.T) {
|
||||
{`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`},
|
||||
} {
|
||||
_, err := ParseManifest([]byte(c.manifest))
|
||||
if c.says == "" {
|
||||
if err != nil {
|
||||
t.Errorf("%s was refused: %v", c.manifest, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("%s was accepted", c.manifest)
|
||||
continue
|
||||
@@ -2356,18 +2356,19 @@ func TestAPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **And a language that runs anywhere must not name one.** A system that decides nothing reads as
|
||||
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045).
|
||||
func TestAPortableBundleMayNotNameASystem(t *testing.T) {
|
||||
got := buildProblemsOf(t, bundleFor("typescript", "arch"))
|
||||
if !strings.Contains(got, "runs anywhere") {
|
||||
t.Fatalf("a portable bundle was allowed to name a system:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPortableBundleNamingNoSystemIsAccepted(t *testing.T) {
|
||||
if got := buildProblemsOf(t, bundleFor("typescript", "")); got != "" {
|
||||
t.Fatalf("an ordinary bundle was refused:\n%s", got)
|
||||
// **A language that runs anywhere must not name a system.** A system that decides nothing reads as
|
||||
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045); a compiled
|
||||
// bundle names one, and a portable bundle naming none is an ordinary one.
|
||||
func TestABundleNamesASystemOnlyWhereItCompiles(t *testing.T) {
|
||||
for _, c := range []struct{ language, system, refused string }{
|
||||
{"go", "arch", ""},
|
||||
{"typescript", "", ""},
|
||||
{"typescript", "arch", "runs anywhere"},
|
||||
} {
|
||||
got := buildProblemsOf(t, bundleFor(c.language, c.system))
|
||||
if (c.refused == "") != (got == "") || !strings.Contains(got, c.refused) {
|
||||
t.Errorf("a %s bundle naming %q: %q, want %q", c.language, c.system, got, c.refused)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2882,13 +2883,23 @@ func TestAStateNameIsLocalAndOneToken(t *testing.T) {
|
||||
{`{"module":"a","version":"1","state":["mesh.servers"]}`, `keeps state "mesh.servers": a state is named locally`},
|
||||
{`{"module":"a","version":"1","state":["my_servers"]}`, `keeps state "my_servers"`},
|
||||
{`{"module":"a","version":"1","state":["s","s"]}`, `keeps state "s" twice`},
|
||||
{`{"module":"a","version":"1","state":[{"name":"s","history":65}]}`, `a key keeps between 1 and 64`},
|
||||
{`{"module":"a","version":"1","state":[{"name":"s","history":65}]}`, `a key keeps 0 or 1 to 64 (0 is taken as 1)`},
|
||||
{`{"module":"a","version":"1","state":[{"name":"s","history":0}]}`, ``}, // 0 passes, as before (ADR 0579 §1)
|
||||
// 1966/AC9: one name, one form
|
||||
{`{"module":"a","version":"1","state":["x"],"requires":[{"name":"x","storage":"key-value","template":"state"}]}`,
|
||||
`a's bus state "x": the name is also kept in the old section state`},
|
||||
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
|
||||
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
|
||||
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
|
||||
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl}`},
|
||||
} {
|
||||
_, err := ParseManifest([]byte(c.manifest))
|
||||
if c.says == "" {
|
||||
if err != nil {
|
||||
t.Errorf("%s was refused: %v", c.manifest, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("%s was accepted", c.manifest)
|
||||
continue
|
||||
|
||||
@@ -84,12 +84,13 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
// 1126: placement is the seat mesh-placement's (ADR 0567): the indexer holds no placement state. ADR 0578 §1: it says
|
||||
// a refused manifest as manifest-refused, its one event.
|
||||
if !reflect.DeepEqual(m.Emits, []string{"manifest-refused"}) {
|
||||
t.Errorf("emits = %v, want manifest-refused alone", m.Emits)
|
||||
// a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as
|
||||
// manifest-accepted.
|
||||
if !reflect.DeepEqual(m.Emits, []string{"manifest-refused", "manifest-accepted"}) {
|
||||
t.Errorf("emits = %v, want manifest-refused and manifest-accepted", m.Emits)
|
||||
}
|
||||
// 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard.
|
||||
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}}; !reflect.DeepEqual(m.State, want) {
|
||||
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}}; !reflect.DeepEqual(m.State, want) {
|
||||
t.Errorf("state = %+v, want %+v", m.State, want)
|
||||
}
|
||||
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-assigner.placement", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
@@ -109,13 +110,12 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
|
||||
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
|
||||
// commit is in another (issue 1900).
|
||||
if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "seat:node-engine.composition"}; !reflect.DeepEqual(m.Invokes, want) {
|
||||
if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls"}; !reflect.DeepEqual(m.Invokes, want) {
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
|
||||
// reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state
|
||||
// (indexer_judge; issue 774, clause 3).
|
||||
if want := []string{"indexer_take_in", "indexer_verdicts", "indexer_judge"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. 1966/AC11:
|
||||
// indexer_old_sections reads the count of the old sections (state oldsections).
|
||||
if want := []string{"indexer_take_in", "indexer_old_sections"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// No streams, no bus, no seat (ADR 0437 §4); it says no event.
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"slug": "indexer",
|
||||
"tools": [
|
||||
"indexer_take_in",
|
||||
"indexer_verdicts",
|
||||
"indexer_judge"
|
||||
"indexer_old_sections"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-build-go.toolchain.reported",
|
||||
@@ -23,10 +22,12 @@
|
||||
{ "name": "taken", "history": 1 },
|
||||
{ "name": "takeins", "history": 1 },
|
||||
{ "name": "verdicts", "history": 5 },
|
||||
{ "name": "oldsections", "history": 1 },
|
||||
{ "name": "packages", "history": 5 }
|
||||
],
|
||||
"emits": [
|
||||
"manifest-refused"
|
||||
"manifest-refused",
|
||||
"manifest-accepted"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-build-go.toolchains",
|
||||
@@ -40,8 +41,7 @@
|
||||
"gitea.gitea_commit_trees",
|
||||
"gitea.gitea_contains",
|
||||
"gitea.gitea_get_file",
|
||||
"gitea.gitea_open_pulls",
|
||||
"seat:node-engine.composition"
|
||||
"gitea.gitea_open_pulls"
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "${dir:state}/broker"
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-teller
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-view
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-watchman
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -59,18 +59,6 @@ func TestTheWatchmansFindings(t *testing.T) {
|
||||
t.Fatalf("findings = %v", o.findings)
|
||||
}
|
||||
w.ModuleVerdict("node-tools", value(""), Put)
|
||||
// Clause 3 of the record on a release's Approve: a manifest that waits for a judge that knows its fields is its own
|
||||
// warning (a refusal in the value, naming the fields), not manifest-refused, and goes when it is accepted.
|
||||
waiting, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64),
|
||||
Repository: "novox/mesh-catalog", Commit: "c", Verdict: delivery.VerdictRefused, Problems: []string{"waits"}, Unknown: []string{"core-credentials"}})
|
||||
w.ModuleVerdict("nats", waiting, Put)
|
||||
if f := o.findings[FindingKey(ScopeModule, "nats", KindManifestWaits)]; f.Severity != Warning || !strings.Contains(f.Summary, "core-credentials") {
|
||||
t.Fatalf("findings = %v", o.findings)
|
||||
}
|
||||
if _, refused := o.findings[FindingKey(ScopeModule, "nats", KindManifestRefused)]; refused {
|
||||
t.Fatalf("a waiting manifest was found refused: %v", o.findings)
|
||||
}
|
||||
w.ModuleVerdict("nats", value(""), Put)
|
||||
}, map[string]string{}},
|
||||
{"WM4: a stuck key not moved is its finding, and its move deletes it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
|
||||
w.Step("go", "n1.ask1", runningStep("ask1", t0), Put)
|
||||
|
||||
@@ -14,11 +14,6 @@ const KindManifestRefused = "manifest-refused"
|
||||
// (novox/hq ADR 0579 §1): it stays accepted and runs; the warning stands until the manifest is mended.
|
||||
const KindManifestOutgrown = delivery.ManifestOutgrown
|
||||
|
||||
// KindManifestWaits is a module whose manifest carries a field the indexer's judge does not know yet (novox/hq ADR 0578
|
||||
// §1, amended): every reader holds it as a refused one and keeps what runs, and the warning stands until a judge knows the
|
||||
// field or the manifest is mended.
|
||||
const KindManifestWaits = delivery.ManifestWaits
|
||||
|
||||
// ModuleVerdict is a change of the indexer's value of a module (delivery.ModulesBucket): a value carrying a refusal is a
|
||||
// finding, one that does not (accepted, or not judged yet) or a deleted key ends it.
|
||||
func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
|
||||
@@ -26,11 +21,9 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
|
||||
defer w.mu.Unlock()
|
||||
key := FindingKey(ScopeModule, module, KindManifestRefused)
|
||||
outgrown := FindingKey(ScopeModule, module, KindManifestOutgrown)
|
||||
waits := FindingKey(ScopeModule, module, KindManifestWaits)
|
||||
if op != Put {
|
||||
w.unfind(key)
|
||||
w.unfind(outgrown)
|
||||
w.unfind(waits)
|
||||
return
|
||||
}
|
||||
m, err := delivery.DecodeModule(value)
|
||||
@@ -39,20 +32,6 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
|
||||
return
|
||||
}
|
||||
v := m.VerdictOf(module)
|
||||
if v != nil && v.Waits() {
|
||||
w.unfind(key)
|
||||
w.unfind(outgrown)
|
||||
w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestWaits, Severity: Warning, Source: delivery.ModulesBucket,
|
||||
Summary: v.Why(),
|
||||
Said: strings.Join(v.Unknown, "; "),
|
||||
Headline: "The manifest of " + module + " waits for a judge that knows its fields",
|
||||
Explanation: "The manifest carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet, so every " +
|
||||
"reader holds " + module + " as a refused one and keeps what runs; an indexer that knows the fields judges it again " +
|
||||
"at its start, and the newest build of the indexer refuses a field it does not know.",
|
||||
Needs: "nothing while a newer indexer is on its way; else a pull request that mends the manifest of " + module})
|
||||
return
|
||||
}
|
||||
w.unfind(waits)
|
||||
if refused, _ := delivery.TakeManifestVerdict(v); !refused {
|
||||
w.unfind(key)
|
||||
if v == nil || len(v.Warnings) == 0 {
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-enrolment
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-grants
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 h1:T66tg9T/1hHg6KN/bDScHPT41IHkxLAGGO8VJiAOlb8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -680,22 +680,6 @@ func TestGrants(t *testing.T) {
|
||||
if slices.Contains(web.Publish, "$JS.API.DIRECT.GET.KV_mesh-knowledge_sections.>") {
|
||||
t.Error("the object bucket's read is granted as a state's")
|
||||
}
|
||||
// ADR 0601 clause 3: the owner's trunk manifest now waits for a field; its running build's manifest still declares the
|
||||
// bucket, so web keeps its object grant.
|
||||
fp := "src2:" + strings.Repeat("cd", 32)
|
||||
b.put(grants.ReadModules, "mesh-knowledge", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
||||
Manifest: delivery.ManifestDigest([]byte(`{"module":"mesh-knowledge","version":"2"}`)), Repository: "novox/x", Commit: "c",
|
||||
Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
|
||||
b.put(grants.ReadHave, "a.mesh-knowledge", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp})
|
||||
heldKey, _ := delivery.BuilderBuildKey("mesh-knowledge", fp)
|
||||
b.put(grants.ReadBuilds, heldKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(knowledge))})
|
||||
if heldWeb := (func() grants.User {
|
||||
us := shadowOf(t, b)
|
||||
return us[slices.IndexFunc(us, func(u grants.User) bool { return u.User == "a.web" })]
|
||||
})(); !slices.Contains(heldWeb.Publish, read) {
|
||||
t.Errorf("a reader of a held module's object store lost its grant: %v", heldWeb.Publish)
|
||||
}
|
||||
b.manifestOf("mesh-knowledge", knowledge) // accepted again for the rest of the row
|
||||
// ADR 0592 §6, §25: the bucket past 75 percent of its cap is said, in one warning, and cleared once below; the history
|
||||
// stream at 10 percent is not.
|
||||
var said []asks.Condition
|
||||
@@ -755,53 +739,6 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// ADR 0601 clause 3: a held module's seats, hearings and reads are composed from the manifest its running build was judged
|
||||
// on, so a principal that claims a seat it defines keeps its grants while its trunk manifest is refused or waits.
|
||||
t.Run("a held module's seat is composed from the manifest its running build was judged on", func(t *testing.T) {
|
||||
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
|
||||
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
|
||||
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
|
||||
const telegram = `{"module":"telegram","version":"1","runs-as":"telegram","resources":[{"id":"account","type":"user","name":"telegram","shell":"/usr/bin/nologin","home":"/var/lib/telegram"}],"own-secrets":{"broker":"/var/lib/t/broker"},"secrets-owner":"telegram",` +
|
||||
`"claims":[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]}`
|
||||
setup := func(held, runs bool) *fakeBus {
|
||||
b := fixture()
|
||||
b.manifestOf("messenger", router)
|
||||
b.manifestOf("telegram", telegram)
|
||||
b.assign("a", "messenger")
|
||||
b.assign("b", "telegram")
|
||||
if held {
|
||||
// The trunk's manifest waits for a field; the build that runs was judged on the manifest before.
|
||||
b.put(grants.ReadModules, "messenger", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
||||
Manifest: delivery.ManifestDigest([]byte(`{"module":"messenger","version":"2"}`)), Repository: "novox/x", Commit: "c",
|
||||
Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
|
||||
if runs {
|
||||
fp := "src2:" + strings.Repeat("cd", 32)
|
||||
digest := delivery.ManifestDigest([]byte(router))
|
||||
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "messenger", JSON: router})
|
||||
b.put(grants.ReadHave, "a.messenger", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp})
|
||||
key, _ := delivery.BuilderBuildKey("messenger", fp)
|
||||
b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest})
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
grantsOf := func(b *fakeBus) string {
|
||||
for _, u := range shadowOf(t, b) {
|
||||
if u.User == "b.telegram" {
|
||||
return strings.Join(u.Publish, ",") + "|" + strings.Join(u.Subscribe, ",")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
accepted, held, unseen := grantsOf(setup(false, false)), grantsOf(setup(true, true)), grantsOf(setup(true, false))
|
||||
if accepted == "" || held != accepted {
|
||||
t.Errorf("a caller of a held module's seat lost grants:\n%s\nwas\n%s", held, accepted)
|
||||
}
|
||||
if unseen == accepted {
|
||||
t.Errorf("the row proves nothing: the grants are the same without the held module's running manifest")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
|
||||
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
|
||||
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
|
||||
@@ -1550,7 +1487,7 @@ func TestGrants(t *testing.T) {
|
||||
fp := partOn(b)
|
||||
fp.Objects = p.Objects
|
||||
fp.Say = func(c asks.Condition) error {
|
||||
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition && c.Key != grants.HeldModuleCondition { // their own rows
|
||||
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows
|
||||
fresh = append(fresh, c)
|
||||
}
|
||||
return nil
|
||||
@@ -2265,6 +2202,28 @@ func TestGrants(t *testing.T) {
|
||||
if res := bare.Compose(); res.Accounts != nil || !strings.Contains(strings.Join(res.Errors, " "), "no bus address") {
|
||||
t.Errorf("a part with no bus address wrote %+v: %v", res.Accounts, res.Errors)
|
||||
}
|
||||
// 1966/AC20 (issue 1966 D5, D5b; novox/hq ADR 0597 §9): a module asking for a ledger in `requires` is wanted a ledger
|
||||
// account on each machine it is assigned to, minted by the vault like every kind and held in the tool runner's
|
||||
// `ledgers`; the account is retired once the module leaves that machine.
|
||||
l := fixture()
|
||||
l.manifestOf("mod-x", strings.Replace(xManifest, `"state"`, `"requires":[{"name":"records","storage":"ledger","template":"ledger",`+
|
||||
`"types":[{"name":"decision","schema":{},"operations":[{"name":"propose","effect":"set-fields"}]}]}],"state"`, 1))
|
||||
l.assign("b", "mod-x")
|
||||
l.loaded = shadowOf(t, l)
|
||||
lp := partOn(l)
|
||||
lp.Bus = grants.BusAddress{URL: "nats://a.mesh:4222"}
|
||||
lp.Compose()
|
||||
for _, user := range []string{"ledger.a.mod-x", "ledger.b.mod-x"} {
|
||||
w, err := buscompose.ReadWantedAccount(user, l.own[grants.StateAccounts][user])
|
||||
if err != nil || w.Kind != buscompose.KindLedger || w.OwnSecret() != "ledgers" || w.Module != "mod-x" {
|
||||
t.Errorf("%s is wanted as %+v (%v)", user, w, err)
|
||||
}
|
||||
}
|
||||
l.unassign("b", "mod-x")
|
||||
if res := lp.Compose(); res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-x", "ledger.b.mod-x"}) ||
|
||||
l.own[grants.StateAccounts]["ledger.a.mod-x"] == nil {
|
||||
t.Errorf("after mod-x left b: %+v", res.Accounts)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC5 (novox/hq ADR 0561 §1): the vault's account-issued wakes a pass, so the next list carries the new user's hash
|
||||
@@ -2432,7 +2391,7 @@ func TestGrants(t *testing.T) {
|
||||
var said, behind []asks.Condition
|
||||
p.Say = func(c asks.Condition) error {
|
||||
switch c.Key {
|
||||
case grants.GrantMissingCondition, grants.RefusedCondition, grants.HeldModuleCondition, grants.FullCondition: // their own rows
|
||||
case grants.GrantMissingCondition, grants.RefusedCondition, grants.FullCondition: // their own rows
|
||||
case grants.ConsumerBehindCondition:
|
||||
behind = append(behind, c)
|
||||
default:
|
||||
@@ -2731,91 +2690,6 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("the refusal was not cleared once it composes: %v, said %+v", res.PrincipalsRefused, said)
|
||||
}
|
||||
})
|
||||
|
||||
// Clause 3 of the record on a release's Approve (issue 774): a module whose manifest waits for a judge that knows a field
|
||||
// of it, or is refused, and whose want names no build, keeps its users of the list in force and is said in a condition;
|
||||
// the rest of the list is written as ever, and the users go the day the manifest is accepted and the module unassigned.
|
||||
t.Run("a module whose manifest waits or is refused keeps its users of the list in force", func(t *testing.T) {
|
||||
b := fixture()
|
||||
const ctl = `{"module":"mod-r","version":"1","own-secrets":{"broker":"/var/lib/r/broker","broker-mod-r-tools":"${dir:tools-secret}/broker-mod-r-tools"},` +
|
||||
`"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/mod-r-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools",` +
|
||||
`"kind":"bundle","language":"go","system":"arch","from":"cmd/mod-r-tools","binary":"mod-r-tools","loads":["mod-r-tools"],"bus":{}}]}}`
|
||||
b.manifestOf("mod-r", ctl)
|
||||
b.assign("b", "mod-r")
|
||||
for _, u := range shadowOf(t, b) {
|
||||
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
|
||||
}
|
||||
b.put(grants.ReadAccounts, "b.mod-r.mod-r-tools", map[string]any{"user": "b.mod-r.mod-r-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
|
||||
busKey, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
||||
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}})
|
||||
// mod-r runs a build whose manifest is the accepted one: the held module's seats and streams are composed from it.
|
||||
runFP := "src2:" + strings.Repeat("cd", 32)
|
||||
b.put(grants.ReadHave, "b.mod-r", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: runFP})
|
||||
runKey, _ := delivery.BuilderBuildKey("mod-r", runFP)
|
||||
b.put(grants.ReadBuilds, runKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(ctl))})
|
||||
p := partOn(b)
|
||||
p.Objects = &fakeObjects{}
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error {
|
||||
if c.Key == grants.HeldModuleCondition {
|
||||
said = append(said, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
p.Compose()
|
||||
inForce, _, _ := p.Sealed()
|
||||
before, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools"))
|
||||
if !ok || len(said) != 1 || said[0].State != asks.StateCleared {
|
||||
t.Fatalf("the list in force keeps no grants of b.mod-r.mod-r-tools (%v), or a held module was said before: %+v", ok, said)
|
||||
}
|
||||
for gen, held := range []struct{ name, unknown string }{{"waits", "core-credentials"}, {"refused", ""}} {
|
||||
// Another user's hash changes in the same pass, so a list that is written shows the held module did not hold it.
|
||||
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": fmt.Sprintf("$2a$10$gen%d", gen), "generation": gen + 2, "fingerprint": fmt.Sprintf("sha256:%d", gen+2)})
|
||||
was, _, _ := p.Sealed()
|
||||
var unknown []string
|
||||
if held.unknown != "" {
|
||||
unknown = []string{held.unknown}
|
||||
}
|
||||
b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
||||
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
|
||||
Problems: []string{"it is held"}, Unknown: unknown})
|
||||
b.put(grants.ReadModules, "mod-q", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
||||
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c"})
|
||||
res := p.Compose()
|
||||
now, _, _ := p.Sealed()
|
||||
after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mod-r.mod-r-tools"))
|
||||
if now.Digest == was.Digest || slices.ContainsFunc(res.Errors, func(e string) bool { return strings.Contains(e, "composed whole") }) {
|
||||
t.Errorf("%s: a held module held the whole list: sealed %+v", held.name, res.Sealed)
|
||||
}
|
||||
if !ok || string(after) != string(before) || len(res.PrincipalsRefused) != 0 {
|
||||
t.Errorf("%s: b.mod-r.mod-r-tools did not keep its grants of the list in force: %s, was %s", held.name, after, before)
|
||||
}
|
||||
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "b.mod-r.mod-r-tools (stays in the list") {
|
||||
t.Errorf("%s: the kept user was not said: %+v", held.name, last)
|
||||
}
|
||||
}
|
||||
// A held module that runs no build the part can read declares nothing to anyone: the list in force is kept, said.
|
||||
delete(b.states[grants.ReadHave], "b.mod-r")
|
||||
b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
||||
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
|
||||
Problems: []string{"it is held"}, Unknown: []string{"core-credentials"}})
|
||||
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen9", "generation": 9, "fingerprint": "sha256:9"})
|
||||
wasList, _, _ := p.Sealed()
|
||||
res := p.Compose()
|
||||
nowList, _, _ := p.Sealed()
|
||||
if nowList.Digest != wasList.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "running manifest of mod-r") {
|
||||
t.Errorf("a held module with no running manifest did not keep the list in force: %+v", res.Sealed)
|
||||
}
|
||||
// Accepted again: the users compose from the manifest and the condition clears.
|
||||
b.manifestOf("mod-r", ctl)
|
||||
if p.Compose(); said[len(said)-1].State != asks.StateCleared {
|
||||
t.Errorf("the condition was not cleared once the manifest is accepted: %+v", said[len(said)-1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fakeObjects is the part's object bucket of sealed lists.
|
||||
|
||||
@@ -50,7 +50,8 @@ type ClaimedSeat struct {
|
||||
// WantedAccount is one key of the grants part's state `accounts`: an account the mesh needs, without its password.
|
||||
type WantedAccount struct {
|
||||
User string `json:"user"`
|
||||
// Kind is KindModule (KindNodeTools for the runtime, named as a module is), KindProcess or KindProvisioner.
|
||||
// Kind is KindModule (KindNodeTools for the runtime, named as a module is), KindProcess, KindProvisioner or
|
||||
// KindLedger.
|
||||
Kind Kind `json:"kind"`
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
@@ -65,8 +66,12 @@ type WantedAccount struct {
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// LedgersSecret is the tool runner's own secret holding its machine's ledger accounts, a map of module to credential
|
||||
// (ADR 0597 §9).
|
||||
const LedgersSecret = "ledgers"
|
||||
|
||||
// mintedKinds are the kinds of account the vault mints (ADR 0561 §1).
|
||||
var mintedKinds = []Kind{KindModule, KindNodeTools, KindProcess, KindProvisioner}
|
||||
var mintedKinds = []Kind{KindModule, KindNodeTools, KindProcess, KindProvisioner, KindLedger}
|
||||
|
||||
// principal is the account as the composer names it.
|
||||
func (w WantedAccount) principal() Principal {
|
||||
@@ -112,9 +117,12 @@ func (w WantedAccount) Same(o WantedAccount) bool {
|
||||
}
|
||||
|
||||
// OwnSecret is the module's own secret the account is held in: `broker`, `broker-<process id>` or `provisioner-broker`
|
||||
// (the controller's names, ADR 0436 §2, ADR 0349 §11).
|
||||
// (the controller's names, ADR 0436 §2, ADR 0349 §11); a ledger account in the tool runner's `ledgers`, which keeps every
|
||||
// ledger account of its machine, by module (ADR 0597 §9).
|
||||
func (w WantedAccount) OwnSecret() string {
|
||||
switch w.Kind {
|
||||
case KindLedger:
|
||||
return LedgersSecret
|
||||
case KindProcess:
|
||||
return "broker-" + w.Process
|
||||
case KindProvisioner:
|
||||
|
||||
@@ -49,6 +49,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
t.Run("only declared streams are created", theGrantsPartCreatesOnlyDeclaredStreams)
|
||||
t.Run("a declared stream is created as the controller asserted it", declaredStreamsAreCreatedAsTheControllerAssertsThem)
|
||||
t.Run("every node's compositions are read by their named readers alone and written by their node", everyNodesCompositions)
|
||||
t.Run("a module's ledger is written by its ledger account alone", aLedgerIsWrittenByItsLedgerAccountAlone)
|
||||
// ADR 0567 §3, §9: a node reads every key of the seat's placement and its candidates (it composes with the whole
|
||||
// proposal laid over), and none of its open proposals; it writes neither.
|
||||
reads := strings.Join(NodeComposeReads("laptop"), " ")
|
||||
@@ -942,3 +943,75 @@ func everyNodesCompositions(t *testing.T) {
|
||||
t.Errorf("the view's membership issues %+v", m.State)
|
||||
}
|
||||
}
|
||||
|
||||
// 1966/AC20, AC21 (issue 1966 D5, ADR 0597 §9): a module asking for a ledger, assigned on two machines, gets one ledger account
|
||||
// on each, publishing only on that module's ledger histories and snapshots; the account is gone from a machine the module
|
||||
// leaves; and the module's own account, its process and the machine's tool runner hold no publish on its ledger.
|
||||
func aLedgerIsWrittenByItsLedgerAccountAlone(t *testing.T) {
|
||||
assigned := func(nodes ...string) []Principal {
|
||||
r := Records{Nodes: nodes, Assigned: map[string][]Declared{}}
|
||||
for _, n := range nodes {
|
||||
r.Assigned[n] = []Declared{
|
||||
{Module: RuntimeModule},
|
||||
{Module: "m", Ledgers: []string{"records", "audit"}, Processes: []ProcessPrincipal{{Process: "w"}}},
|
||||
{Module: "plain"},
|
||||
}
|
||||
}
|
||||
users, err := Users(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return users
|
||||
}
|
||||
ledgers := func(users []Principal) []string {
|
||||
var out []string
|
||||
for _, p := range users {
|
||||
if p.Kind == KindLedger {
|
||||
out = append(out, p.Username())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
both := assigned("a", "b")
|
||||
if got := ledgers(both); !slices.Equal(got, []string{"ledger.a.m", "ledger.b.m"}) {
|
||||
t.Fatalf("ledger accounts %v, want ledger.a.m and ledger.b.m", got)
|
||||
}
|
||||
if got := ledgers(assigned("a")); !slices.Equal(got, []string{"ledger.a.m"}) {
|
||||
t.Fatalf("after m left b: ledger accounts %v, want ledger.a.m alone", got)
|
||||
}
|
||||
want := []string{"mesh.ledger.m.audit.history.>", "mesh.ledger.m.audit.snapshot.>",
|
||||
"mesh.ledger.m.records.history.>", "mesh.ledger.m.records.snapshot.>"}
|
||||
raw := []string{"mesh.ledger.m.records.history.decision/0290", "mesh.ledger.m.records.snapshot.decision/0290"}
|
||||
for _, p := range both {
|
||||
perms, err := PermissionsFor(p)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", p.Username(), err)
|
||||
}
|
||||
if p.Kind == KindLedger {
|
||||
if !slices.Equal(perms.Publish, want) || len(perms.PublishDeny) != 0 {
|
||||
t.Errorf("%s may publish %v, want %v alone", p.Username(), perms.Publish, want)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, pattern := range perms.Publish {
|
||||
for _, subject := range raw {
|
||||
if SubjectsOverlap(pattern, subject) {
|
||||
t.Errorf("%s may publish %s, which reaches the ledger subject %s", p.Username(), pattern, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.Kind == KindModule || p.Kind == KindNodeTools || p.Kind == KindProcess {
|
||||
if err := CheckWriters(p, want); err == nil {
|
||||
t.Errorf("%s was granted a raw publish on a ledger's history", p.Username())
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := CheckWriters(Principal{Kind: KindLedger, Node: "a", Module: "other", Ledgers: []string{"records"}}, want); err == nil {
|
||||
t.Error("another module's ledger account was granted m's ledger histories")
|
||||
}
|
||||
for _, bad := range []Principal{{Kind: KindLedger, Node: "a", Module: "m"}, {Kind: KindLedger, Node: "a", Module: "m", Ledgers: []string{"a.b"}}} {
|
||||
if _, err := PermissionsFor(bad); err == nil {
|
||||
t.Errorf("%+v was composed", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
)
|
||||
|
||||
// A module's ledger and the one account that writes it (issue 1966 D5; novox/hq ADR 0597 §9, design 25 § 12.4).
|
||||
//
|
||||
// **Only the tool runner's ledger credential publishes on a ledger's history** (ADR 0597 §9): one per module assignment,
|
||||
// named ledger.<machine>.<module> (KindLedger), scoped to the subjects of that module's ledger histories and snapshots, and
|
||||
// gone when the module is unassigned from the machine. The module's own account, its processes and the machine's tool
|
||||
// runner hold no publish there, so a raw publish on a history is refused by the bus. The subjects are named in one place, mesh-sdk's
|
||||
// buscompose (LedgerHistorySubject, LedgerSnapshotSubject), `mesh.ledger.<module>.<local name>.history|snapshot` and
|
||||
// one subject per instance below it (design 25 § 12.4).
|
||||
|
||||
// LedgerPublishGrants are the publish patterns a module's ledger account is granted: the histories and the snapshots of the
|
||||
// ledgers named, every instance of each, and nothing else. Sorted.
|
||||
func LedgerPublishGrants(module string, locals []string) []string {
|
||||
var out []string
|
||||
for _, local := range locals {
|
||||
out = append(out, sdk.LedgerHistorySubject(module, local)+".>", sdk.LedgerSnapshotSubject(module, local)+".>")
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// writesLedger is a module's ledger account publishing on that module's own ledgers' histories and snapshots, and no other
|
||||
// principal (ADR 0597 §9).
|
||||
func writesLedger(p Principal, pattern string) bool {
|
||||
return p.Kind == KindLedger && slices.Contains(LedgerPublishGrants(p.Module, p.Ledgers), pattern)
|
||||
}
|
||||
|
||||
// ledgerSubjects are the subjects no principal but a ledger account may publish on, for the writers table.
|
||||
var ledgerSubjects = []string{"mesh.ledger.*.*.history.>", "mesh.ledger.*.*.snapshot.>"}
|
||||
|
||||
// ledgerPermissions are a ledger account's whole authority: publishing on its module's ledgers, and its own reply space for
|
||||
// the stream's acknowledgements. Refused when it names no ledger or a name that is no subject token.
|
||||
func ledgerPermissions(p Principal) (Permissions, error) {
|
||||
pub := LedgerPublishGrants(p.Module, p.Ledgers)
|
||||
if len(p.Ledgers) == 0 {
|
||||
return Permissions{}, fmt.Errorf("%s asks for no ledger, so its account would publish nowhere", p.Username())
|
||||
}
|
||||
for _, l := range p.Ledgers {
|
||||
if !safeSubject.MatchString(l) || strings.Contains(l, "_") {
|
||||
return Permissions{}, fmt.Errorf("%s: %q cannot be part of a subject: a permission is a subject pattern, and this would widen it", p.Username(), l)
|
||||
}
|
||||
}
|
||||
if err := CheckWriters(p, pub); err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
|
||||
}
|
||||
@@ -52,6 +52,10 @@ const (
|
||||
// exactly what its bus says — never root, never the operator account, and never able to answer a tool
|
||||
// call or a seat's verb (§3). One per process and machine.
|
||||
KindProcess Kind = "process"
|
||||
// KindLedger is one module assignment's ledger credential (novox/hq ADR 0597 §9, design 25 § 12.4): held by that
|
||||
// machine's tool runner, never by the module's own code, and the only account that publishes on that module's ledger
|
||||
// histories and snapshots. One per module assignment, whose module asks for a ledger in `requires`.
|
||||
KindLedger Kind = "ledger"
|
||||
)
|
||||
|
||||
// RuntimeModule is the module that IS the node's tool runtime (novox/hq ADR 0175). Where it is
|
||||
@@ -102,6 +106,9 @@ type Principal struct {
|
||||
Kind Kind
|
||||
Node string
|
||||
Module string
|
||||
// Ledgers are the local names of the ledgers its module asks for in `requires`, for a KindLedger principal alone
|
||||
// (issue 1966 D5): its whole authority is publishing on those ledgers' histories and snapshots (LedgerPublishGrants).
|
||||
Ledgers []string
|
||||
// Process is the process resource's id, for a KindProcess principal alone (novox/hq ADR 0436 §2):
|
||||
// Username and inbox are <node>.<module>.<process id>.
|
||||
Process string
|
||||
@@ -425,6 +432,10 @@ func (p Principal) Username() string {
|
||||
return p.Login
|
||||
}
|
||||
return "node." + p.Node
|
||||
case KindLedger:
|
||||
// `ledger.<machine>.<module>`: never `<node>.<module>`, which is the module's own account or the tool runner's,
|
||||
// so the module's code never holds it.
|
||||
return "ledger." + p.Node + "." + p.Module
|
||||
case KindProvisioner:
|
||||
// Named for the machine, as a node is: one client per machine, whichever module runs it, and never
|
||||
// `<node>.<module>`, which is the module's own account or the tool runner's.
|
||||
@@ -567,6 +578,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
|
||||
}
|
||||
|
||||
// A ledger account publishes on its module's ledger histories and snapshots and nothing else (issue 1966 D5).
|
||||
if p.Kind == KindLedger {
|
||||
return ledgerPermissions(p)
|
||||
}
|
||||
|
||||
if p.Kind == KindProcess && len(p.Removes) > 0 {
|
||||
pub, err := BusRemovalGrants(p.Removes, p.FilesFor)
|
||||
if err != nil {
|
||||
|
||||
@@ -103,6 +103,9 @@ type Declared struct {
|
||||
// composed whether or not the module itself has an account (NoAccount), because a process account is
|
||||
// delivered in its own secret, broker-<process id>, never the module's `broker`.
|
||||
Processes []ProcessPrincipal
|
||||
// Ledgers are the local names of the ledgers it asks for in `requires` (catalogue.StorageLedger); each assignment of a
|
||||
// module with any gets a ledger principal of its own (issue 1966 D5, ADR 0597 §9).
|
||||
Ledgers []string
|
||||
}
|
||||
|
||||
// ProcessPrincipal is one process of a module that declares bus (novox/hq ADR 0436 §1, §2), as the broker
|
||||
@@ -229,6 +232,11 @@ func Users(r Records) ([]Principal, error) {
|
||||
out[len(out)-1].FilesFor = sortedCopy(r.Nodes)
|
||||
}
|
||||
}
|
||||
// **A ledger account per assignment of a module that asks for a ledger** (issue 1966 D5, ADR 0597 §9): held by the
|
||||
// machine's tool runner, so it is composed whether or not the module has an account of its own.
|
||||
if len(d.Ledgers) > 0 {
|
||||
out = append(out, Principal{Kind: KindLedger, Node: node, Module: d.Module, Ledgers: sortedCopy(d.Ledgers)})
|
||||
}
|
||||
if runtimeHere && d.Module == RuntimeModule {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -236,6 +236,12 @@ var WritersTable = []WriterRow{
|
||||
ProvisionerAppliedBucket,
|
||||
Others: "the watchman reads it; a failure is that machine's failed apply",
|
||||
Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner},
|
||||
// A ledger's history and its snapshots (novox/hq ADR 0597 §9, issue 1966 D5): the tool runner's ledger credential of
|
||||
// the owning module alone, so the module's own code, its processes and the runner's own account cannot write one raw.
|
||||
{State: "a ledger's history and snapshots", Writer: "the tool runner's ledger credential of the owning module",
|
||||
KeptIn: "the bus, the ledger's history stream and its snapshot subject",
|
||||
Others: "the owner reads them through the tool runner's calls; nobody else reads them (ADR 0596 §2)",
|
||||
Subjects: ledgerSubjects, Writes: writesLedger},
|
||||
}
|
||||
|
||||
// CheckWriters refuses a grant that lets a principal publish on a subject the writers table gives
|
||||
|
||||
@@ -46,8 +46,9 @@ type BusAddress struct {
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address. A
|
||||
// node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
|
||||
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address: a
|
||||
// module's, a process's, a machine's provisioner client's and a module assignment's ledger account (issue 1966 D5b, ADR
|
||||
// 0597 §9). A node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
|
||||
func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[string]buscompose.WantedAccount, error) {
|
||||
principals, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
@@ -62,7 +63,7 @@ func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[str
|
||||
if m, ok := manifestOn(in, p.Node, p.Module); ok {
|
||||
w.Claims = ClaimsOf(m)
|
||||
}
|
||||
case buscompose.KindProcess, buscompose.KindProvisioner:
|
||||
case buscompose.KindProcess, buscompose.KindProvisioner, buscompose.KindLedger:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// **A module whose manifest waits or is refused keeps its bus users** (clause 3 of the record on a release's Approve; issue
|
||||
// 774). The indexer holds a manifest it refuses, or that waits for a judge that knows a field of it, out of the manifests
|
||||
// the part composes from (inputs.go, Refused), so a module with no want naming a build had no manifest to derive its users
|
||||
// from and its users left the list: the bus module's accounts among them, which locked its tools out. Now such a module's
|
||||
// users, the ones the list in force carries, stay in the list as it holds them, the same way a refused principal's do
|
||||
// (refused.go), and the part says so as the warning mesh-grants.module-held until the manifest is accepted. One module's
|
||||
// manifest never holds the list: every other principal is composed as ever. A module whose want names a build is composed
|
||||
// from the manifest of that build (Inputs.Running), not kept.
|
||||
|
||||
// HeldModuleCondition is the key of the warning raised while a module's manifest is held and its users are kept.
|
||||
const HeldModuleCondition = "mesh-grants.module-held"
|
||||
|
||||
// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that
|
||||
// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of
|
||||
// a module on a node is `<node>.<module>` or `<node>.<module>.<process>` (buscompose.Principal.Username). Pure.
|
||||
func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
||||
held := map[string]string{}
|
||||
for _, g := range in.Refused {
|
||||
if module, ok := strings.CutPrefix(g.Input, "manifest of "); ok {
|
||||
held[module] = g.Why
|
||||
}
|
||||
}
|
||||
out := map[string]string{}
|
||||
if len(held) == 0 {
|
||||
return out
|
||||
}
|
||||
for node, modules := range in.Assigned {
|
||||
for _, module := range modules {
|
||||
why, ok := held[module]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, runs := in.Running[node+"."+module]; runs {
|
||||
continue
|
||||
}
|
||||
prefix := node + "." + module
|
||||
for _, user := range candidates {
|
||||
if user == prefix || strings.HasPrefix(user, prefix+".") {
|
||||
out[user] = fmt.Sprintf("the manifest of %s is held: %s", module, why)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// inForceGrants are the grants of each user of the list in force as the part sealed them, or nil where it kept none.
|
||||
func (p *Part) inForceGrants() map[string]User {
|
||||
if g, _, had := p.Sealed(); had && g.Digest != "" && p.Store != nil {
|
||||
inForce, _ := p.sealedGrantsOf(g.Digest)
|
||||
return inForce
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergedRefusals is refused and held in one map, for the parts that keep a user's grants of the list in force.
|
||||
func mergedRefusals(refused, held map[string]string) map[string]string {
|
||||
if len(held) == 0 {
|
||||
return refused
|
||||
}
|
||||
out := make(map[string]string, len(refused)+len(held))
|
||||
for u, why := range held {
|
||||
out[u] = why
|
||||
}
|
||||
for u, why := range refused {
|
||||
out[u] = why
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sayHeldModules raises HeldModuleCondition while a held module's users are kept, naming each user with what became of it,
|
||||
// and clears it once none is; said once per set, again when it changes, and at the first pass after a start.
|
||||
func (p *Part) sayHeldModules(held map[string]string, kept map[string]User) {
|
||||
if p.Say == nil {
|
||||
return
|
||||
}
|
||||
var lines []string
|
||||
users := make([]string, 0, len(held))
|
||||
for u := range held {
|
||||
users = append(users, u)
|
||||
}
|
||||
sort.Strings(users)
|
||||
for _, user := range users {
|
||||
became := "not in the list in force and left out"
|
||||
if _, ok := kept[user]; ok {
|
||||
became = "stays in the list as the list in force holds it"
|
||||
}
|
||||
lines = append(lines, user+" ("+became+"): "+held[user])
|
||||
}
|
||||
why := strings.Join(lines, "; ")
|
||||
p.holdMu.Lock()
|
||||
defer p.holdMu.Unlock()
|
||||
if p.heldModulesKnown && why == p.heldModulesSaid {
|
||||
return
|
||||
}
|
||||
c := asks.Condition{Key: HeldModuleCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
||||
if why == "" {
|
||||
c.State = asks.StateCleared
|
||||
} else {
|
||||
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
||||
c.Summary = fmt.Sprintf("%d bus user(s) of a module whose manifest is held are kept in the list: %s", len(users), why)
|
||||
c.Headline, c.Needs = "A module's manifest is held, its bus users are kept", "a manifest the indexer accepts, or a judge that knows its fields"
|
||||
c.Explanation = asks.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one judged by an indexer that knows its fields). " +
|
||||
"The rest of the user list is written as ever; a held module's users stay as the list in force holds them: " + why
|
||||
c.Resolved = "every module's manifest is accepted"
|
||||
}
|
||||
if p.Say(c) == nil {
|
||||
p.heldModulesSaid, p.heldModulesKnown = why, true
|
||||
}
|
||||
}
|
||||
@@ -131,11 +131,7 @@ type Inputs struct {
|
||||
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
|
||||
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
|
||||
Refused []Gap
|
||||
// Held is, by module, the manifest a held module's running build was judged on (its have's fingerprint, the builder's
|
||||
// build of it): what the seats it defines, the streams others hear from it and its reads are composed from while its
|
||||
// trunk manifest is refused or waits, so no other principal loses a grant to it (ADR 0601 clause 3).
|
||||
Held map[string]catalogue.Manifest
|
||||
Gaps []Gap
|
||||
Gaps []Gap
|
||||
}
|
||||
|
||||
// HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said by the part, and
|
||||
@@ -357,8 +353,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
}
|
||||
var m catalogue.Manifest
|
||||
if err == nil {
|
||||
// The indexer's refusal, or its wait for a judge that knows a field, carried in its value (ADR 0578 §3): held and named as a refused manifest.
|
||||
if verdict := v.VerdictOf(module); verdict != nil && verdict.Held() {
|
||||
// The indexer's refusal, carried in its value (ADR 0578 §3): held and named as a refused manifest.
|
||||
if verdict := v.VerdictOf(module); verdict != nil && verdict.Refused() {
|
||||
err = errors.New(verdict.Why())
|
||||
}
|
||||
}
|
||||
@@ -444,23 +440,6 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
sort.Strings(fromTrunk)
|
||||
in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")})
|
||||
}
|
||||
in.Held = heldManifests(r, in, cache)
|
||||
// A held module placed on a node whose running manifest is not found declares no seat, stream or object bucket to
|
||||
// anyone: composing past it would drop the grants of those who use them, so the list in force is kept (ADR 0601
|
||||
// clause 3), and the gap says which module and why.
|
||||
for _, g := range in.Refused {
|
||||
module, ok := strings.CutPrefix(g.Input, "manifest of ")
|
||||
if _, found := in.Held[module]; !ok || found {
|
||||
continue
|
||||
}
|
||||
for _, assigned := range in.Assigned {
|
||||
if slices.Contains(assigned, module) {
|
||||
in.Gaps = append(in.Gaps, Gap{Input: "running manifest of " + module, Failed: true,
|
||||
Why: "its manifest is held and no running build's manifest is found, so the seats and streams it declares cannot be composed: the list in force is kept"})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
in.Hashes = readHashes(r, &in.Gaps)
|
||||
// The seat mesh-enrolment's keys are every token since the controller's end (issue 1126).
|
||||
in.Enrolling, _ = readEnrolling(r, &in.Gaps)
|
||||
@@ -572,81 +551,16 @@ func sortGaps(g []Gap) {
|
||||
sort.SliceStable(g, func(i, j int) bool { return strings.Compare(g[i].Input, g[j].Input) < 0 })
|
||||
}
|
||||
|
||||
// ReadFailed names every read this pass could not make: what a list composed now may lack. A manifest the indexer holds
|
||||
// (refused, or waiting for a judge that knows a field of it) is not one: it holds its module's users in the list as the
|
||||
// list in force has them (held.go), and never holds the list, so one manifest never freezes the bus's users (issue 774).
|
||||
func (in Inputs) ReadFailed() []string {
|
||||
// ReadFailures names every read this pass could not make and every manifest refused: what a list composed now may lack.
|
||||
func (in Inputs) ReadFailures() []string {
|
||||
var out []string
|
||||
for _, g := range in.Gaps {
|
||||
if g.Failed {
|
||||
out = append(out, g.Input+": "+g.Why)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ReadFailures is ReadFailed and every manifest refused or waiting: what the part retires nothing on (the accounts it
|
||||
// keeps, the hearings it withholds).
|
||||
func (in Inputs) ReadFailures() []string {
|
||||
out := in.ReadFailed()
|
||||
for _, g := range in.Refused {
|
||||
out = append(out, g.Input+": "+g.Why)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// heldManifests are the manifests the running builds of the held modules (in.Refused) were judged on, by module: the have
|
||||
// of the module on a node names the fingerprint that runs, and the builder's build of it the manifest. A module that runs
|
||||
// nowhere, or whose build or manifest cannot be read, has none, and that is said as a gap.
|
||||
func heldManifests(r Reader, in Inputs, cache *Manifests) map[string]catalogue.Manifest {
|
||||
out := map[string]catalogue.Manifest{}
|
||||
for _, g := range in.Refused {
|
||||
module, ok := strings.CutPrefix(g.Input, "manifest of ")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, node := range in.Nodes {
|
||||
raw, found, err := r.Get(ReadHave, node+"."+module)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
h, err := delivery.DecodeHave(raw)
|
||||
if err != nil || h.FP == "" {
|
||||
continue
|
||||
}
|
||||
bk, err := delivery.BuilderBuildKey(module, h.FP)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
braw, found, err := r.Get(ReadBuilds, bk)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
b, err := delivery.DecodeBuilderBuild(braw)
|
||||
if err != nil || b.Manifest == "" {
|
||||
continue
|
||||
}
|
||||
if m, err := cache.get(r, b.Manifest); err == nil {
|
||||
out[module] = m
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// declared are the manifests the seats, hearings and reads are composed from: the trunk's, and for a held module the
|
||||
// manifest its running build was judged on (Held).
|
||||
func (in Inputs) declared() map[string]catalogue.Manifest {
|
||||
if len(in.Held) == 0 {
|
||||
return in.Manifests
|
||||
}
|
||||
out := make(map[string]catalogue.Manifest, len(in.Manifests)+len(in.Held))
|
||||
for m, man := range in.Held {
|
||||
out[m] = man
|
||||
}
|
||||
for m, man := range in.Manifests {
|
||||
out[m] = man
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -189,9 +189,6 @@ type Part struct {
|
||||
// refusedSaid is the refused principals last said as a condition, once refusedKnown (refused.go).
|
||||
refusedSaid string
|
||||
refusedKnown bool
|
||||
// heldModulesSaid is the held modules' kept users last said as a condition, once heldModulesKnown (held.go).
|
||||
heldModulesSaid string
|
||||
heldModulesKnown bool
|
||||
// missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go).
|
||||
missingSaid string
|
||||
missingKnown bool
|
||||
@@ -262,24 +259,14 @@ func (p *Part) Compose() Result {
|
||||
res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error())
|
||||
}
|
||||
// A refused principal keeps its grants of the list in force, said once per change (refused.go).
|
||||
// And a module whose manifest is held (refused, or waiting for a judge that knows a field of it) and whose want names no
|
||||
// build keeps its users of the list in force (held.go).
|
||||
loaded, loadErr := LoadedUsers(p.Call)
|
||||
inForce := p.inForceGrants()
|
||||
candidates := names(loaded)
|
||||
for user := range inForce {
|
||||
candidates = append(candidates, user)
|
||||
}
|
||||
heldUsers := heldModuleUsers(in, candidates)
|
||||
allRefused := mergedRefusals(refused, heldUsers)
|
||||
kept, _ := p.keptOf(allRefused)
|
||||
kept, _ := p.keptOf(refused)
|
||||
for _, user := range sortedKeys(refused) {
|
||||
res.PrincipalsRefused = append(res.PrincipalsRefused, user+": "+refused[user])
|
||||
}
|
||||
p.sayPrincipalsRefused(refused, kept)
|
||||
p.sayHeldModules(heldUsers, kept)
|
||||
users = WithHashes(withKept(users, kept), in.Hashes)
|
||||
st.Digest, st.Users = Digest(users), len(users)
|
||||
loaded, loadErr := LoadedUsers(p.Call)
|
||||
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
|
||||
if loadErr != nil {
|
||||
st.Compare.Unread = loadErr.Error()
|
||||
@@ -311,7 +298,7 @@ func (p *Part) Compose() Result {
|
||||
if p.Store != nil {
|
||||
p.writeWithheld(&res, in, recs)
|
||||
}
|
||||
list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, allRefused, kept)
|
||||
list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, refused, kept)
|
||||
// The grants the assigner reads (ADR 0547 §4, §5): one that adds a permission only once the bus loaded a list that
|
||||
// carries it (issue 774).
|
||||
if p.Store != nil {
|
||||
|
||||
@@ -27,8 +27,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
var gaps []Gap
|
||||
seats := map[string]catalogue.SeatDeclaration{}
|
||||
declarers := map[string]string{}
|
||||
declared := in.declared()
|
||||
for _, m := range declared {
|
||||
for _, m := range in.Manifests {
|
||||
for _, s := range m.DefinesSeats {
|
||||
seats[s.Name] = s
|
||||
declarers[s.Name] = m.Module
|
||||
@@ -80,8 +79,8 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
for _, sr := range catalogue.ReadsGranted(m, here, node) {
|
||||
d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key})
|
||||
}
|
||||
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes)
|
||||
d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, declared, values)
|
||||
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, in.Manifests, node, values, d.Consumes)
|
||||
d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values)
|
||||
return d
|
||||
}
|
||||
d := declare(m)
|
||||
@@ -102,7 +101,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
}
|
||||
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
|
||||
// part; it adds no principal, since each node's engine speaks as the node's account.
|
||||
if m, ok := declared[buscompose.EngineModule]; ok {
|
||||
if m, ok := in.Manifests[buscompose.EngineModule]; ok {
|
||||
out.Engine = &buscompose.Declared{Module: m.Module, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
|
||||
}
|
||||
return out, gaps
|
||||
@@ -140,6 +139,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
Served: catalogue.ServedSettings(m),
|
||||
// Its own object buckets (ADR 0592 §5).
|
||||
Objects: objectsOf(m),
|
||||
// The ledgers it asks for in `requires` (issue 1966 D5): each assignment gets a ledger account.
|
||||
Ledgers: ledgersOf(m),
|
||||
}
|
||||
d.Loads = loadsABundle(m)
|
||||
if _, reads := m.OwnSecrets["broker"]; !reads {
|
||||
@@ -283,6 +284,17 @@ func readsOfObjects(module, node string, reads, refused []string, manifests map[
|
||||
return states, objects, refusedOut
|
||||
}
|
||||
|
||||
// ledgersOf are the local names of the ledgers a manifest asks for in `requires`.
|
||||
func ledgersOf(m catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for _, e := range m.BusState {
|
||||
if e.Storage == catalogue.StorageLedger {
|
||||
out = append(out, e.Name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func logsOf(m catalogue.Manifest) []buscompose.Log {
|
||||
var out []buscompose.Log
|
||||
for _, l := range m.Logs {
|
||||
|
||||
@@ -299,7 +299,7 @@ func listUsers(list []byte) []string {
|
||||
// they could not be read). Empty when it may.
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost []string, digest string) string {
|
||||
p.hashHold = nil
|
||||
if f := in.ReadFailed(); len(f) > 0 {
|
||||
if f := in.ReadFailures(); len(f) > 0 {
|
||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||
}
|
||||
if bus == nil {
|
||||
|
||||
Reference in New Issue
Block a user