Compare commits

..
Author SHA1 Message Date
jschoubben bd8ea8ad4f Format the hand path's live test (issue 1945)
mesh/check pass bd8ea8ad4f30 on 34b105ea88df | unit 0 run, 1497 kept | integration 0 run, 2575 kept | clone 36s | composition passed with v0.1.14-0.20
mesh/delivery ready: it delivers once merged
2026-10-24 18:25:54 +02:00
jschoubben 1350c428ed Let the operator's hand path replace a file over 64 KiB in a node's object bucket (issue 1945; the review of A1, H1)
The object store reads an object's metadata and purges its old chunks when it replaces one, so the hand path is
granted the direct and message gets and the purge of each node's object stream; a test on a bus running the composed
user list walks the hand path end to end.
2026-10-24 18:25:46 +02:00
jschoubben 7f8e5a821c Grant the controller's bucket-deleted, narrow a node's backup word to restic's, and grant the operator release 6's hand path ahead of the switch (issue 1945; the review of A1)
The new controller says its first start's deletions as an event, a node-engine hears only its backup holder, and the
way back right after the switch, a want and a composition written at the terminal, runs on the user list today's
controller composes (ADR 0539 clauses 2, 10 and 13). The operator is the administrator person, the one second
writer ADR 0539 names.
2026-10-24 18:21:58 +02:00
jschoubben 3ae2d6e17c Grant release 6's compositions, wants and backups ahead of its switch (issue 1945)
The switch to release 6 is today's controller's last plan, and the bus runs the user list it composes until the new
controller writes one: so the new controller's writes of each node's compositions and their digests, each new
node-engine's reads of its own compositions and wants and its backup holder's verbs and word, and the assigner's read
of the digests must be granted by this controller already (ADR 0539 clauses 2, 6, 9 and 16; the review's A1).
2026-10-24 18:14:45 +02:00
mesh-tools 34b105ea88 Merge pull request 'The planner puts the new node-engine and the new controller in one tier for release 6's switch, and orders every other plan as today (issue 1942)' (#455) from fix/1942-switch-one-tier into main 2026-10-24 15:37:02 +00:00
jschoubben 44d9d40582 Switch: acts only on a plan moving both engine and controller, and only for value 6 (issue 1942)
mesh/delivery delivered
mesh/check checking 44d9d4058253 on 34b105ea88df
2026-10-24 16:26:35 +02:00
jschoubben 74bed5a800 Switch plan: engine and controller share one tier when a merge names release 6's switch (issue 1942, ADR 0539 §11) 2026-10-24 16:15:52 +02:00
29 changed files with 606 additions and 47 deletions
+29 -5
View File
@@ -1210,13 +1210,14 @@ func planBatch(ctx context.Context, open *stores, batch *inventory.Plan, carry [
}
}
sort.Strings(also)
ordered, err := changeOrderEdges(ctx, merges, members, entries, moved, edges)
ordered, together, err := changeOrderEdges(ctx, merges, members, entries, moved, edges)
if err != nil {
return err
}
var withheld []string
plan := planOfMoves(moved, append(append([]inventory.Edge{}, edges...), ordered...))
placedBy(&plan, ordered)
placedTogether(&plan, together)
// **Withheld, a stopped merge's modules** (novox/hq ADR 0299): a module is built at its branch, which holds the
// merge a stop left out of the walk these merges were walked again from — built here only as a dependent, it
// would deliver that merge. One a merge of this batch moves is built: that merge carries the stopped change,
@@ -1341,6 +1342,19 @@ func placedBy(plan *inventory.Plan, ordered []inventory.Edge) {
}
}
// placedTogether writes the switch's line on the controller's and the node-engine's tier (ADR 0539 §11), so the plan
// says why they share one: the record and each edge overruled.
func placedTogether(plan *inventory.Plan, together string) {
if together == "" {
return
}
for _, n := range []string{theController, theEngine} {
if m, ok := plan.Modules[n]; ok {
m.Placed = strings.Trim(m.Placed+"; "+together, "; ")
}
}
}
// unchangedSaid names the modules a walk says matched what runs, for its note.
func unchangedSaid(unchanged map[string]string) string {
names := make([]string, 0, len(unchanged))
@@ -1536,7 +1550,7 @@ type batchMember struct {
// A cycle is said, never lost: the plan puts the modules left in its last tier, and the composed check fails the
// group before it merges.
func changeOrderEdges(ctx context.Context, merges []inventory.BatchedMerge, members []batchMember,
entries []inventory.Entry, moved []string, edges []inventory.Edge) ([]inventory.Edge, error) {
entries []inventory.Entry, moved []string, edges []inventory.Edge) ([]inventory.Edge, string, error) {
// What each repository's branch held before the batch: the first parent of its earliest merge in it.
earliest := map[string]inventory.BatchedMerge{}
for _, k := range merges {
@@ -1559,17 +1573,27 @@ func changeOrderEdges(ctx context.Context, merges []inventory.BatchedMerge, memb
}
changes, err := changesOf(ctx, sources)
if err != nil {
return nil, fmt.Errorf("what the merges say of their manifests cannot be read, so the order of their modules "+
return nil, "", fmt.Errorf("what the merges say of their manifests cannot be read, so the order of their modules "+
"cannot be drawn: %w", err)
}
order := orderEdges(moved, changes, edges)
named := false
for _, k := range merges {
var e link.SourceMoved
if json.Unmarshal(k.Event, &e) == nil && switchNamed(e.Body) {
named = true
}
}
order := orderEdges(moved, changes, edges, named)
for _, e := range order.Edges {
fmt.Printf(" order: %s\n", edgeSaid(e))
}
if order.Together != "" {
fmt.Printf(" order: %s\n", order.Together)
}
if said := order.CycleSaid(); said != "" {
fmt.Printf(" %s\n", said)
}
return order.Edges, nil
return order.Edges, order.Together, nil
}
// searchAfterFailures starts the search a failed walk leaves (ADR 0276 decision 3): every merge it answered
+1 -1
View File
@@ -711,7 +711,7 @@ func orderOf(members []orderMember, reach map[string]orderReach, edges []invento
}
}
}
order := orderEdges(all, changes, edges)
order := orderEdges(all, changes, edges, false)
out.Edges = order.Edges
out.CycleSaid = order.CycleSaid()
for _, e := range order.Edges {
+44 -2
View File
@@ -20,6 +20,11 @@ const (
ruleEngineFirst = "engine before controller"
)
// switchField is the line of a pull request's description that names release 6's switch (novox/hq ADR 0539 §11):
// "Release-switch: 6". A plan whose merges include a pull request with it puts the node-engine and the controller in
// one tier. Read by switchNamed, from the description the forge announced with the merge.
const switchField = "Release-switch:"
// The two modules the rollout rules name.
const (
theController = "mesh-controller"
@@ -39,6 +44,20 @@ type planOrder struct {
Edges []inventory.Edge
// Cycle is every edge of a cycle among the order's edges and the graph's, the module graph's included.
Cycle []inventory.Edge
// Together is, when the switch is named and the plan moves the controller, the line its tier carries (ADR 0539
// §11): the record and each edge it overruled (ADR 0419 clause 5). Empty otherwise.
Together string
}
// switchNamed says whether a pull request's description names release 6's switch: a line that is exactly the field
// with the value 6.
func switchNamed(body string) bool {
for _, line := range strings.Split(body, "\n") {
if rest, ok := strings.CutPrefix(strings.TrimSpace(line), switchField); ok && strings.TrimSpace(rest) == "6" {
return true
}
}
return false
}
// CycleSaid names every module and edge of the cycle, for a check to fail with (ADR 0419 clause 6).
@@ -72,10 +91,14 @@ func orders(e inventory.Edge) bool {
// it, goes before every module whose manifest the plan changes, the node-engine excepted; and the node-engine goes
// before the controller by default, when the plan moves both and no edge orders them either way, transitively. An
// edge is "From after To". graph is the module graph, whose edges order too and are never drawn again.
func orderEdges(moved []string, changes map[string]moduleChange, graph []inventory.Edge) planOrder {
// switchNamed is release 6's switch (ADR 0539 §11): the version-skew edge and the engine-first default are not drawn
// and the plan says it overruled them (Together); the verb edges and the graph still order.
func orderEdges(moved []string, changes map[string]moduleChange, graph []inventory.Edge, switchNamed bool) planOrder {
var out planOrder
sorted := append([]string(nil), moved...)
sort.Strings(sorted)
// The switch acts only on a plan that moves both the engine and the controller.
switchNamed = switchNamed && slices.Contains(sorted, theController) && slices.Contains(sorted, theEngine)
for _, server := range sorted {
for _, caller := range sorted {
if server == caller {
@@ -89,15 +112,25 @@ func orderEdges(moved []string, changes map[string]moduleChange, graph []invento
}
}
}
var overruled []string
if slices.Contains(sorted, theController) {
for _, m := range sorted {
if m != theController && m != theEngine && changes[m].Manifest {
if switchNamed {
overruled = append(overruled, ruleVersionSkew+" ("+m+" after "+theController+")")
continue
}
out.Edges = append(out.Edges, inventory.Edge{From: m, To: theController, Kind: edgeChangeOrder,
Why: ruleVersionSkew})
}
}
}
if slices.Contains(sorted, theController) && slices.Contains(sorted, theEngine) {
if switchNamed {
overruled = append(overruled, ruleEngineFirst)
}
if switchNamed {
out.Together = switchSaid(overruled)
} else if slices.Contains(sorted, theController) && slices.Contains(sorted, theEngine) {
all := orderingEdges(graph, out.Edges)
switch {
case pathBetween(all, theController, theEngine) != nil:
@@ -127,6 +160,15 @@ func orderEdges(moved []string, changes map[string]moduleChange, graph []invento
return out
}
// switchSaid is the line of a tier the switch shapes: the record that decided it and each edge it overruled.
func switchSaid(overruled []string) string {
said := "release 6's switch (ADR 0539 §11)"
if len(overruled) > 0 {
said += " overrules " + strings.Join(overruled, ", ") + " (ADR 0419 clause 5)"
}
return said
}
// orderingEdges is the graph's edges that order and the edges drawn, together.
func orderingEdges(graph, drawn []inventory.Edge) []inventory.Edge {
var out []inventory.Edge
+69 -8
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"slices"
"strings"
"testing"
@@ -28,7 +29,7 @@ func TestAVerbIsServedBeforeItsCallerIsSent(t *testing.T) {
"server": {Serves: []string{"mesh-delivery.groups"}, Manifest: true},
"caller": {Invokes: []string{"mesh-delivery.groups"}, Manifest: true},
}
got := orderEdges([]string{"caller", "server"}, changes, nil)
got := orderEdges([]string{"caller", "server"}, changes, nil, false)
e, ok := hasEdge(got.Edges, "caller", "server")
if !ok || !strings.Contains(e.Why, "verb before caller") || !strings.Contains(e.Why, "mesh-delivery.groups") {
t.Fatalf("the caller does not follow the server by the verb: %+v", got.Edges)
@@ -47,7 +48,7 @@ func TestEdgesBothWaysAreACycleNamingEveryModuleAndEdge(t *testing.T) {
"x": {Serves: []string{"s.a"}, Invokes: []string{"s.b"}},
"y": {Serves: []string{"s.b"}, Invokes: []string{"s.a"}},
}
got := orderEdges([]string{"x", "y"}, changes, nil)
got := orderEdges([]string{"x", "y"}, changes, nil, false)
if len(got.Cycle) < 2 {
t.Fatalf("no cycle found: %+v", got)
}
@@ -62,7 +63,7 @@ func TestEdgesBothWaysAreACycleNamingEveryModuleAndEdge(t *testing.T) {
// The node-engine goes before the controller by default, and says so; an edge of the manifests the other way
// overrules it, and the edge that did is said.
func TestTheEngineGoesBeforeTheControllerUnlessAnEdgeSaysOtherwise(t *testing.T) {
got := orderEdges([]string{"mesh-controller", "mesh-host"}, nil, nil)
got := orderEdges([]string{"mesh-controller", "mesh-host"}, nil, nil, false)
e, ok := hasEdge(got.Edges, "mesh-controller", "mesh-host")
if !ok || e.Why != "engine before controller" {
t.Fatalf("the engine does not go first: %+v", got.Edges)
@@ -71,7 +72,7 @@ func TestTheEngineGoesBeforeTheControllerUnlessAnEdgeSaysOtherwise(t *testing.T)
"mesh-host": {Serves: []string{"s.v"}},
"mesh-controller": {Invokes: []string{"s.v"}},
}
got = orderEdges([]string{"mesh-controller", "mesh-host"}, changes, nil)
got = orderEdges([]string{"mesh-controller", "mesh-host"}, changes, nil, false)
if _, ok := hasEdge(got.Edges, "mesh-controller", "mesh-host"); !ok || len(got.Edges) != 1 ||
!strings.Contains(got.Edges[0].Why, "verb before caller") {
t.Fatalf("the verb edge does not place the controller after the engine: %+v", got.Edges)
@@ -81,7 +82,7 @@ func TestTheEngineGoesBeforeTheControllerUnlessAnEdgeSaysOtherwise(t *testing.T)
"mesh-controller": {Serves: []string{"s.v"}},
"mesh-host": {Invokes: []string{"s.v"}},
}
got = orderEdges([]string{"mesh-controller", "mesh-host"}, changes, nil)
got = orderEdges([]string{"mesh-controller", "mesh-host"}, changes, nil, false)
e, ok = hasEdge(got.Edges, "mesh-host", "mesh-controller")
if !ok || len(got.Edges) != 1 || !strings.Contains(e.Why, "overrules engine before controller") {
t.Fatalf("the verb edge does not say it overruled the default: %+v", got.Edges)
@@ -91,14 +92,14 @@ func TestTheEngineGoesBeforeTheControllerUnlessAnEdgeSaysOtherwise(t *testing.T)
// The controller goes before every module whose manifest the plan changes (version skew), the node-engine excepted.
func TestTheControllerGoesBeforeEveryChangedManifest(t *testing.T) {
changes := map[string]moduleChange{"app": {Manifest: true}, "other": {}}
got := orderEdges([]string{"mesh-controller", "app", "other"}, changes, nil)
got := orderEdges([]string{"mesh-controller", "app", "other"}, changes, nil, false)
if e, ok := hasEdge(got.Edges, "app", "mesh-controller"); !ok || e.Why != "version skew" {
t.Fatalf("the changed manifest does not follow the controller: %+v", got.Edges)
}
if _, ok := hasEdge(got.Edges, "other", "mesh-controller"); ok {
t.Fatalf("a module with an unchanged manifest was ordered: %+v", got.Edges)
}
if got := orderEdges([]string{"app", "other"}, changes, nil); len(got.Edges) != 0 {
if got := orderEdges([]string{"app", "other"}, changes, nil, false); len(got.Edges) != 0 {
t.Fatalf("version skew ordered a plan that does not move the controller: %+v", got.Edges)
}
}
@@ -109,7 +110,7 @@ func TestTheOrderIsPerModuleNotPerRepository(t *testing.T) {
"a": {Serves: []string{"s.v"}},
"c": {Invokes: []string{"s.v"}},
}
got := orderEdges([]string{"a", "b", "c"}, changes, nil)
got := orderEdges([]string{"a", "b", "c"}, changes, nil, false)
tiers := tiersOf([]string{"a", "b", "c"}, got.Edges)
if len(tiers) != 2 || strings.Join(tiers[0], ",") != "a,b" || strings.Join(tiers[1], ",") != "c" {
t.Fatalf("tiers %v, want [a b] then [c]", tiers)
@@ -181,3 +182,63 @@ func TestATruncatedChangeIsReadAsChangingTheManifestsOfWhatItMoves(t *testing.T)
t.Fatalf("the member's change is read as %+v", src)
}
}
// 1942/AC1: release 6's switch puts the node-engine and the controller in one tier; without it the controller's
// tier comes first as today (ADR 0539 §11).
func TestTheSwitchPutsTheEngineAndTheControllerInOneTier(t *testing.T) {
moved := []string{"mesh-controller", "mesh-host", "app"}
changes := map[string]moduleChange{"mesh-host": {Manifest: true}, "app": {Manifest: true}}
with := orderEdges(moved, changes, nil, true)
if len(with.Edges) != 0 || len(with.Cycle) != 0 {
t.Fatalf("the switch still draws edges: %+v", with)
}
if tiers := tiersOf(moved, with.Edges); len(tiers) != 1 || len(tiers[0]) != 3 {
t.Fatalf("tiers %v, want one tier", tiers)
}
without := orderEdges(moved, changes, nil, false)
tiers := tiersOf(moved, without.Edges)
if len(tiers) < 2 || slices.Contains(tiers[0], "mesh-controller") && slices.Contains(tiers[0], "mesh-host") {
t.Fatalf("without the switch the tiers are %v, want the engine's tier before the controller's", tiers)
}
if without.Together != "" {
t.Fatalf("a plan without the switch carries its line: %q", without.Together)
}
// A verb edge between them still orders them.
verb := map[string]moduleChange{"mesh-host": {Serves: []string{"s.v"}}, "mesh-controller": {Invokes: []string{"s.v"}}}
if got := orderEdges([]string{"mesh-controller", "mesh-host"}, verb, nil, true); len(got.Edges) != 1 {
t.Fatalf("the switch dropped a verb edge: %+v", got.Edges)
}
}
// 1942/AC2: the shared tier's line names the record and each edge overruled.
func TestTheSwitchSaysTheEdgesItOverruled(t *testing.T) {
changes := map[string]moduleChange{"app": {Manifest: true}}
got := orderEdges([]string{"mesh-controller", "mesh-host", "app"}, changes, nil, true)
for _, want := range []string{"ADR 0539 §11", "engine before controller", "version skew (app after mesh-controller)", "ADR 0419"} {
if !strings.Contains(got.Together, want) {
t.Fatalf("the line %q lacks %q", got.Together, want)
}
}
plan := inventory.Plan{Modules: map[string]*inventory.PlanModule{"mesh-controller": {}, "mesh-host": {}}}
placedTogether(&plan, got.Together)
if plan.Modules["mesh-controller"].Placed != got.Together || plan.Modules["mesh-host"].Placed != got.Together {
t.Fatalf("the tier does not carry the line: %+v %+v", plan.Modules["mesh-controller"], plan.Modules["mesh-host"])
}
}
// The switch is named by a line of a pull request's description.
func TestTheSwitchIsNamedByAFieldOfTheDescription(t *testing.T) {
if !switchNamed("Fixes 1\n\nRelease-switch: 6\n") || switchNamed("Release-switch:\n") || switchNamed("Release-switch: 7") || switchNamed("about the Release-switch: 6") {
t.Fatal("the field is not read as a line with a value")
}
}
// 1942/AC3: the switch acts only on a plan that moves the engine and the controller: without mesh-host the
// version-skew edge stays, the line present or not.
func TestTheSwitchWithoutTheEngineKeepsVersionSkew(t *testing.T) {
changes := map[string]moduleChange{"app": {Manifest: true}}
got := orderEdges([]string{"mesh-controller", "app"}, changes, nil, true)
if e, ok := hasEdge(got.Edges, "app", "mesh-controller"); !ok || e.Why != "version skew" || got.Together != "" {
t.Fatalf("version skew lost without the engine: %+v", got)
}
}
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the repository's root files or other directories): mesh-build-go never keeps this package's verdict (issue 1935).
@@ -193,13 +193,9 @@ func TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToT
if !strings.Contains(moved, "laptop") || strings.Contains(moved, "anchor") {
t.Errorf("1160/AC5: the plan says %q; want laptop named silent and not anchor", moved)
}
// The move to tier 1 is saved in one step and the look that marks b taken from its key is the next (release_plan.go
// advances the tier, plan_keys.go takeFromKey marks it): read b once that look ran, not in the instant the tier moved,
// which a loaded build node shows before it (group check fcb076c5387f).
eventually(t, "tier 1's module taken from its key", func() bool {
b := plan().Modules["b"]
return b != nil && b.Key
})
if b := plan().Modules["b"]; b == nil || !b.Key {
t.Errorf("tier 1's module is not taken from its key: %+v", b)
}
// Tier 1's module is taken from its key once that is built, which the walk's look does by itself.
heldB.Store(false)
askToLook()
+1 -1
View File
@@ -20,7 +20,7 @@ const StatesAnnouncedBucket = announce.KeptBucket
// among them, which the pacman module reads, ADR 0507 §5), the chain's buckets and the grants issued, which the assigner
// reads (ADR 0528 clause 2).
var controllerReadable = []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts,
MirrorDeclaredPackages, ControllerGrants}
MirrorDeclaredPackages, ControllerGrants, ControllerComposed}
// ControllerAnnouncement is what the controller announces it keeps (ADR 0495 clause 2): every bucket of its own by its local
// name, the ones others may read marked readable. It announces itself, as any module does from its code.
+1 -1
View File
@@ -39,7 +39,7 @@ func TestTheControllerAnnouncesWhatItKeeps(t *testing.T) {
}
}
want := []string{ChainBuilds, MirrorDeclaredConfig, MirrorPredefined, ChainRollouts,
MirrorAssignments, MirrorDeclaredPackages, ControllerGrants}
MirrorAssignments, MirrorDeclaredPackages, ControllerGrants, ControllerComposed}
slices.Sort(want)
if !slices.Equal(readable, want) {
t.Fatalf("readable %v, want %v", readable, want)
+137
View File
@@ -0,0 +1,137 @@
package broker
import (
"context"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go/jetstream"
)
// Release 6's grants, carried ahead of it (novox/hq ADR 0539 clauses 2, 6, 9 and 16; issue 1945, the review's A1). The
// switch to release 6 is the last delivery plan of today's controller, and the bus's user list after it is the one
// today's controller composes: so the subjects the new controller, every new node-engine and the assigner use after the
// switch are granted by this controller already, before the group merges. Unused until then: nothing writes or reads
// these buckets before release 6.
//
// Names as mesh-sdk's release 6 gives them (delivery.ComposedBucketFor, ControllerComposedBucket); written out here
// because this controller's mesh-sdk predates them, and held equal by release 6's own tests.
// ComposedBucketPrefix starts a node's bucket of compositions, `composed-<node>`.
const ComposedBucketPrefix = "composed-"
// ControllerComposed is the local name of the compositions' digests, `mesh-controller_composed`, which the assigner reads.
const ControllerComposed = "composed"
// ControllerComposedBucket is the bucket of the compositions' digests.
var ControllerComposedBucket = BucketName(ControllerSeat, ControllerComposed)
// composedBucketOf is a node's bucket of compositions; a node name a subject would widen is refused.
func composedBucketOf(node string) (string, error) {
if !safeSubject.MatchString(node) {
return "", fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
}
return ComposedBucketPrefix + node, nil
}
// ComposedWrites is what the controller publishes to write a node's compositions: the bucket's own subjects.
func ComposedWrites(node string) (string, error) {
bucket, err := composedBucketOf(node)
if err != nil {
return "", err
}
return "$KV." + bucket + ".>", nil
}
// ComposedReads is what a node's node-engine publishes to follow its wants and compositions: every key of its own
// composition bucket, and its own wants (`want.<node>.<module>`, `want.<node>.node`) in the assigner's bucket; nothing
// of another node's, nor the digests.
func ComposedReads(node string) ([]string, error) {
bucket, err := composedBucketOf(node)
if err != nil {
return nil, err
}
return append(readGrants(bucket, ""), readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...), nil
}
// VerbsTheNodeAgentAsks are the seat verbs a node's node-engine calls of its own machine while it applies a want: its
// backup holder backs a data module up before it moves, and says when it was (ADR 0539 clause 16).
var VerbsTheNodeAgentAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}, {Seat: "node-backup", Verb: "backed-up"}}
// NodeAgentAsks are the subjects of VerbsTheNodeAgentAsks for one node: `mesh.seat.<seat>.tool.<verb>.<node>`.
func NodeAgentAsks(node string) ([]string, error) {
if !safeSubject.MatchString(node) {
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
}
var out []string
for _, v := range VerbsTheNodeAgentAsks {
out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb+"."+node)
}
return out, nil
}
// NodeAgentHears are the events a node's node-engine subscribes while it applies a want: the backup holder's word that a
// backup is taken, from whichever module holds node-backup on its machine.
var NodeAgentHears = []string{"mesh.mod." + BackupHolderModule + ".event.backed-up"}
// BackupHolderModule is the module that holds node-backup on every node, restic: the one whose word a node-engine hears.
// A second holder of the seat is a change of this grant.
const BackupHolderModule = "restic"
// ComposedDigestsConfig is the bucket of the compositions' digests as the controller asserts it: five revisions a key.
func ComposedDigestsConfig() jetstream.KeyValueConfig {
return jetstream.KeyValueConfig{
Bucket: ControllerComposedBucket,
Description: "each composition's digest and the judgement of its move from what the node runs (novox/hq ADR 0539 " +
"clauses 2 and 14): written by the controller alone, read by the assigner; empty until release 6",
History: 5,
MaxValueSize: 4 << 10,
MaxBytes: 64 << 20,
Storage: jetstream.FileStorage,
}
}
// EnsureComposedDigests asserts the bucket of the compositions' digests.
func EnsureComposedDigests(ctx context.Context, js jetstream.JetStream) error {
if _, err := js.CreateOrUpdateKeyValue(ctx, ComposedDigestsConfig()); err != nil {
return fmt.Errorf("asserting bucket %s: %w", ControllerComposedBucket, err)
}
return nil
}
// administers says a person may invoke every tool (`*`): the operator.
func administers(invokes []string) bool {
for _, i := range invokes {
if i == "*" {
return true
}
}
return false
}
// HandPathWrites is what the operator's account publishes for release 6's hand path (ADR 0539 clauses 2 and 10): a want
// of any node, written and read back, in the assigner's bucket; each named node's compositions; the digests; and each
// node's object bucket, for a file over 64 KiB.
func HandPathWrites(nodes []string) ([]string, error) {
out := append([]string{"$KV." + delivery.AssignerWantsBucket + ".want.>"}, readGrants(delivery.AssignerWantsBucket, "want.>")...)
out = append(out, "$KV."+ControllerComposedBucket+".>")
out = append(out, readGrants(ControllerComposedBucket, "")...)
for _, node := range nodes {
w, err := ComposedWrites(node)
if err != nil {
return nil, err
}
bucket, _ := composedBucketOf(node)
out = append(out, w)
out = append(out, readGrants(bucket, "")...)
files, err := DeclarationFilesWrites(node)
if err != nil {
return nil, err
}
stream := "OBJ_" + DeclarationFilesBucket + node
// The object store's put reads an object's metadata already there and, replacing it, purges its old chunks.
out = append(out, files, "$JS.API.STREAM.INFO."+stream, "$JS.API.DIRECT.GET."+stream+".>",
"$JS.API.STREAM.MSG.GET."+stream, "$JS.API.STREAM.PURGE."+stream)
}
return out, nil
}
+144
View File
@@ -0,0 +1,144 @@
//go:build integration
package broker
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"golang.org/x/crypto/bcrypt"
)
// 1945 (the review of A1, H1): on a bus running the user list this controller composes, the operator's account (a
// person who may invoke every tool) walks release 6's hand path end to end: it writes a node's want, that node's
// composition and its digest, and stores a file over 64 KiB in the node's object bucket and replaces it; a person with
// fewer tools may do none of it.
func TestNatsTheOperatorWritesTheHandPathOnTheComposedUserList(t *testing.T) {
hash, err := bcrypt.GenerateFromPassword([]byte("pw"), bcrypt.MinCost)
if err != nil {
t.Fatal(err)
}
users, err := Users(Records{Nodes: []string{"ace"},
People: map[string][]string{"jochen": {"*"}, "guest": {"mesh-controller.status"}}})
if err != nil {
t.Fatal(err)
}
for i := range users {
users[i].PasswordHash = string(hash)
}
accounts, err := ComposeAccounts(users)
if err != nil {
t.Fatal(err)
}
conf := filepath.Join(t.TempDir(), "bus.conf")
if err := os.WriteFile(conf, []byte("listen: 127.0.0.1:-1\njetstream { store_dir: "+`"`+t.TempDir()+`"`+" }\n"+accounts), 0o600); err != nil {
t.Fatal(err)
}
opts, err := server.ProcessConfigFile(conf)
if err != nil {
t.Fatalf("the composed accounts do not parse: %v", err)
}
opts.NoLog, opts.NoSigs = true, true
s, err := server.NewServer(opts)
if err != nil {
t.Fatal(err)
}
go s.Start()
if !s.ReadyForConnections(10 * time.Second) {
t.Fatal("the bus did not come up")
}
t.Cleanup(s.Shutdown)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
connect := func(user string) jetstream.JetStream {
t.Helper()
nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, "pw"), nats.CustomInboxPrefix("_INBOX."+user))
if err != nil {
t.Fatalf("%s cannot connect: %v", user, err)
}
t.Cleanup(nc.Close)
js, err := jetstream.New(nc)
if err != nil {
t.Fatal(err)
}
return js
}
// The buckets as the controller and the assigner make them.
cjs := connect(ControllerName)
if err := EnsureDeclarationFiles(ctx, cjs, "ace"); err != nil {
t.Fatal(err)
}
if err := EnsureComposedDigests(ctx, cjs); err != nil {
t.Fatal(err)
}
if _, err := cjs.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "composed-ace", History: 5}); err != nil {
t.Fatal(err)
}
// The assigner's bucket, made here by the controller's account, which may create any stream.
if _, err := cjs.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "mesh-delivery-assigner_wants", History: 20}); err != nil {
t.Fatal(err)
}
short := func(c context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(c, 5*time.Second)
}
for _, who := range []struct {
user string
may bool
}{{"person.jochen", true}, {"person.guest", false}} {
js := connect(who.user)
try := func(what string, act func(context.Context) error) {
t.Helper()
c, stop := short(ctx)
defer stop()
err := act(c)
if (err == nil) != who.may {
t.Errorf("%s: %s: %v (may: %v)", who.user, what, err, who.may)
}
}
try("write ace's want", func(c context.Context) error {
kv, err := js.KeyValue(c, "mesh-delivery-assigner_wants")
if err != nil {
return err
}
_, err = kv.Put(c, "want.ace.mesh-controller", []byte(`{"v":1}`))
return err
})
try("write ace's composition", func(c context.Context) error {
kv, err := js.KeyValue(c, "composed-ace")
if err != nil {
return err
}
_, err = kv.Put(c, "ace.mesh-controller.aaaa", []byte(`{"v":1}`))
return err
})
try("write its digest", func(c context.Context) error {
kv, err := js.KeyValue(c, ControllerComposedBucket)
if err != nil {
return err
}
_, err = kv.Put(c, "ace.mesh-controller.aaaa", []byte(`{"v":1}`))
return err
})
try("store and replace a file over 64 KiB", func(c context.Context) error {
store, err := js.ObjectStore(c, "declaration-files-ace")
if err != nil {
return err
}
body := bytes.Repeat([]byte("x"), 200<<10)
if _, err := store.PutBytes(c, "f00d", body); err != nil {
return err
}
_, err = store.PutBytes(c, "f00d", append(body, 'y'))
return err
})
}
}
+88
View File
@@ -0,0 +1,88 @@
package broker
import (
"slices"
"strings"
"testing"
"github.com/nats-io/nats.go/jetstream"
)
// 1945 (the review's A1): the bus's user list today's controller composes, which is the one the bus runs after release
// 6's switch, already grants every subject the new controller, every new node-engine and the assigner use: the
// controller writes each node's compositions and the digests; a node-engine reads its own compositions and wants, asks
// its own backup holder and hears its word, and nothing of another node's; the assigner reads the digests, announced
// readable.
func TestTheUserListBeforeTheSwitchGrantsWhatReleaseSixUses(t *testing.T) {
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x", FilesFor: []string{"ace", "g14"}})
if err != nil {
t.Fatal(err)
}
for _, s := range []string{"$KV.composed-ace.>", "$KV.composed-g14.>", "$KV.mesh-controller_composed.>",
"mesh.seat.mesh-controller.event.bucket-deleted"} {
has(t, controller.Publish, s)
}
node, err := PermissionsFor(Principal{Kind: KindNode, Node: "ace", PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
for _, s := range []string{
"$JS.API.STREAM.INFO.KV_composed-ace", "$JS.API.DIRECT.GET.KV_composed-ace.>", "$JS.API.CONSUMER.CREATE.KV_composed-ace.>",
"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.>",
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.>",
"mesh.seat.node-backup.tool.now.ace", "mesh.seat.node-backup.tool.backed-up.ace",
} {
has(t, node.Publish, s)
}
has(t, node.Subscribe, "mesh.mod.restic.event.backed-up")
for _, p := range node.Publish {
if strings.Contains(p, "composed-g14") || strings.Contains(p, "mesh-controller_composed") || strings.Contains(p, "want.g14") ||
strings.HasPrefix(p, "$KV.composed-") || strings.HasSuffix(p, "node-backup.tool.now.*") {
t.Errorf("a node-engine of ace may publish %s", p)
}
}
if !slices.Contains(controllerReadableStates(), ControllerComposed) ||
!slices.Contains(ControllerBuckets(), ControllerComposedBucket) || !IsControllerBucket(ControllerComposedBucket) {
t.Fatalf("the digests are not announced readable among the controller's buckets")
}
if !ControllerAnnouncement("novox").Readable(ControllerComposed) {
t.Fatalf("the controller does not announce %s readable", ControllerComposed)
}
assigner, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "mesh-delivery-assigner", PasswordHash: "x",
Reads: []string{"mesh-controller.composed"}})
if err != nil {
t.Fatal(err)
}
has(t, assigner.Publish, "$JS.API.STREAM.INFO.KV_mesh-controller_composed")
has(t, assigner.Publish, "$JS.API.CONSUMER.CREATE.KV_mesh-controller_composed.>")
if c := ComposedDigestsConfig(); c.Bucket != "mesh-controller_composed" || c.History != 5 || c.Storage != jetstream.FileStorage {
t.Errorf("the digests' bucket is %+v", c)
}
}
// 1945 (the review of A1, (b)): the operator's account, a person who may invoke every tool, writes release 6's hand
// path: a want of any node, each node's compositions and the digests; a person with fewer tools writes none of it.
func TestTheOperatorWritesTheHandPathAheadOfTheSwitch(t *testing.T) {
users, err := Users(Records{Nodes: []string{"ace", "novox"}, People: map[string][]string{"jochen": {"*"}, "guest": {"mesh-controller.status"}}})
if err != nil {
t.Fatal(err)
}
for _, u := range users {
if u.Kind != KindPerson {
continue
}
perms, err := PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
hand := []string{"$KV.mesh-delivery-assigner_wants.want.>", "$KV.composed-ace.>", "$KV.composed-novox.>",
"$KV.mesh-controller_composed.>", "$O.declaration-files-ace.>",
"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.>", "$JS.API.STREAM.MSG.GET.OBJ_declaration-files-ace",
"$JS.API.STREAM.PURGE.OBJ_declaration-files-ace"}
for _, s := range hand {
if got := slices.Contains(perms.Publish, s); got != (u.Module == "jochen") {
t.Errorf("%s may publish %s: %v", u.Module, s, got)
}
}
}
}
+7 -2
View File
@@ -196,14 +196,15 @@ func IsControllerBucket(bucket string) bool {
bucket == MergeWindowBucket || bucket == RolloutSilentBucket ||
bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket ||
bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket ||
bucket == GrantsBucket
bucket == GrantsBucket || bucket == ControllerComposedBucket
}
// ControllerBuckets are the controller's own buckets, in the order they are asserted.
func ControllerBuckets() []string {
return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket,
AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, DeclaredPackagesBucket, MergeWindowBucket, RolloutSilentBucket,
DeliveryMergesBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket, GrantsBucket}
DeliveryMergesBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket, GrantsBucket,
ControllerComposedBucket}
}
// ControllerBucketsAsserter is what raising the controller's buckets needs of a connection.
@@ -331,6 +332,10 @@ func (j *JetStream) EnsureControllerBuckets() error {
}); err != nil {
return fmt.Errorf("asserting bucket %s: %w", RolloutSilentBucket, err)
}
// **The compositions' digests** (ADR 0539 clauses 2 and 6), asserted ahead of release 6: what its assigner reads.
if err := EnsureComposedDigests(ctx, js); err != nil {
return err
}
// **The states each module announced** (novox/hq ADR 0495 clauses 2 and 3, issue 1790): one key a module, its last word.
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: StatesAnnouncedBucket,
+2 -1
View File
@@ -115,7 +115,8 @@ func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) {
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
rowed := map[string]bool{}
for _, row := range WritersTable {
if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") && row.State != "the grants issued" {
if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") && row.State != "the grants issued" &&
row.State != "the compositions' digests" {
continue
}
for _, subject := range row.Subjects {
+5 -2
View File
@@ -206,8 +206,11 @@ func TestNoPathDeletesALogOrABucket(t *testing.T) {
})
for _, lit := range stringsIn(f) {
if strings.Contains(lit, "$JS.API.STREAM.DELETE") || strings.Contains(lit, "$JS.API.STREAM.PURGE") {
// Only the writers table names it, as a subject no one but the controller may publish.
if !strings.HasSuffix(filepath.ToSlash(path), "internal/broker/writers.go") {
// Only the writers table names it, as a subject no one but the controller may publish; and the operator's
// release 6 hand path, whose object store purges the chunks of an object it replaces in a node's
// object bucket (novox/hq ADR 0539 clause 10): a grant, never a call of this repository.
if !strings.HasSuffix(filepath.ToSlash(path), "internal/broker/writers.go") &&
!strings.HasSuffix(filepath.ToSlash(path), "internal/broker/composed.go") {
found = append(found, path+": "+lit)
}
}
+39
View File
@@ -427,6 +427,16 @@ var DegradableGrants = []DegradableGrant{
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
"first push, and writes the chain only after that, holding the lease (ADR 0408 §2 and §8, issue 1089)",
},
{
Subject: "mesh.seat." + ControllerSeat + ".event.bucket-deleted",
Reason: "refused: nothing is deleted before release 6, whose controller composes its own permissions into the bus " +
"module's composition before its first start's deletions (novox/hq ADR 0539 clause 13)",
},
{
Subject: "$KV." + ControllerComposedBucket + ".>",
Reason: "carried only at genesis: nothing writes a composition's digest before release 6, whose controller " +
"composes its own permissions into the bus module's composition (novox/hq ADR 0539 clauses 2 and 7)",
},
{
Subject: "$KV." + GrantsBucket + ".>",
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
@@ -738,6 +748,12 @@ func PermissionsFor(p Principal) (Permissions, error) {
return Permissions{}, err
}
pub = append(pub, writes)
// And each node's compositions, which it alone writes from release 6 on (novox/hq ADR 0539 clause 2).
composed, err := ComposedWrites(node)
if err != nil {
return Permissions{}, err
}
pub = append(pub, composed)
}
// **And what the bus says about itself, read-only** (novox/hq to-be 45 §3, S9): a durable
// consumer that gave up on a message, or one that was deleted. The server already publishes
@@ -773,6 +789,16 @@ func PermissionsFor(p Principal) (Permissions, error) {
// And may ask what answers (novox/hq ADR 0197): a question every service answers about
// itself, its replies to the asker's own inbox.
pub = append(pub, discovering()...)
// And, for an administrator (`*`, the operator), release 6's hand path (ADR 0539 clauses 2 and 10): a want written
// at the terminal, and a composition made by hand with mesh-compose. Granted ahead of the switch, since the way back
// right after it runs on the user list today's controller composes.
if administers(p.Invokes) {
hand, err := HandPathWrites(p.FilesFor)
if err != nil {
return Permissions{}, err
}
pub = append(pub, hand...)
}
case KindEnrolment:
// A leaked token is useless for anything but enrolling: it cannot read a declaration, hear
@@ -845,6 +871,19 @@ func PermissionsFor(p Principal) (Permissions, error) {
return Permissions{}, err
}
pub = append(pub, fileReads...)
// And, from release 6 on, its own compositions and wants, its own backup holder's verbs and word (novox/hq ADR
// 0539 clauses 2, 9 and 16): granted ahead, since today's controller composes the user list after the switch.
composedReads, err := ComposedReads(p.Node)
if err != nil {
return Permissions{}, err
}
pub = append(pub, composedReads...)
backups, err := NodeAgentAsks(p.Node)
if err != nil {
return Permissions{}, err
}
pub = append(pub, backups...)
sub = append(sub, NodeAgentHears...)
// And, while the vault serves it, the vault's move-in in its own name and nobody else's (issue 676): the
// values sealed to this machine, which only it opens, handed in sealed to the vault's key for the call.
if p.MovesIntoTheVault {
+7 -1
View File
@@ -237,11 +237,17 @@ func TestAnAdministratorMayAskAnyTool(t *testing.T) {
}
// **Nothing but tools.** A person who could publish an event would be able to claim a module
// said something; one who could publish control traffic would be a second controller.
// said something; one who could publish control traffic would be a second controller. The one exception is the
// operator's (an administrator's) release 6 hand path, a want and a composition written by hand (novox/hq ADR 0539
// clauses 2 and 10).
func TestAPersonReachesNothingButTools(t *testing.T) {
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
Invokes: []string{"*"}, PasswordHash: "x"})
hand, _ := HandPathWrites(nil)
for _, p := range perms.Publish {
if slices.Contains(hand, p) {
continue
}
// A tool call — named by its caller or not (novox/hq issue 365) — or asking what answers (novox/hq
// ADR 0197), a question every service answers about itself, which claims nothing and controls nothing.
if !strings.Contains(p, ".tool.") && !strings.Contains(p, ".call.") && !strings.HasPrefix(p, "$SRV.") {
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
+4 -1
View File
@@ -381,7 +381,10 @@ var ControllerStates = []string{"applied", "refused", "built-before",
// each registered module's declared config (design 57 F3).
"assignment-changed", "declared-config-changed", "predefined-changed",
// And a builder's key not taken in because the artifact store does not hold what it names (novox/hq issue 1937).
"artifacts-gone"}
"artifacts-gone",
// And, from release 6 on, a bucket of the plans' state deleted at the new controller's first start, and the
// declarations' stream once no node reads one (novox/hq ADR 0539 clauses 11 and 13): granted ahead of the switch.
"bucket-deleted"}
// BusAdvisories are what the bus server says about the mesh's own account that the controller
// reads (novox/hq to-be 45 §3, S9): a durable consumer that handed a message over as often as it
+3 -3
View File
@@ -29,7 +29,7 @@ accounts {
]
users = [
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.artifacts-gone", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_composed.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.artifacts-gone", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.bucket-deleted", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] }
allow_responses: { max: 1, ttl: "1m" }
} }
@@ -38,8 +38,8 @@ accounts {
subscribe: { allow: ["_INBOX.enrol.one.>"] }
} }
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>"] }
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] }
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$SRV.PING.node-tools.one", "mesh.control.one.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one"] }
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.restic.event.backed-up", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] }
allow_responses: { max: 1, ttl: "1m" }
} }
{ user: "one.mesh-delivery-watchman", password: "$2a$11$wwwwwwwwwwwwwwwwwwwwww", permissions: {
+6 -1
View File
@@ -230,7 +230,12 @@ func Users(r Records) ([]Principal, error) {
out = append(out, Principal{Kind: KindEnrolment, Node: node})
}
for _, person := range sortedNames(r.People) {
out = append(out, Principal{Kind: KindPerson, Module: person, Invokes: r.People[person]})
p := Principal{Kind: KindPerson, Module: person, Invokes: r.People[person]}
if administers(p.Invokes) {
// The operator's hand path from release 6 on writes each node's compositions (ADR 0539 clause 10).
p.FilesFor = sortedCopy(r.Nodes)
}
out = append(out, p)
}
// Refused here rather than discovered by the server. Two users with one name is a file the
+14 -2
View File
@@ -152,6 +152,11 @@ var WritersTable = []WriterRow{
DeliveryMergesBucket, Others: "read through plans",
Subjects: append(append(kvOf(MergeWindowBucket), kvOf(RolloutSilentBucket)...), kvOf(DeliveryMergesBucket)...),
Writes: isController},
// The compositions' digests, granted and asserted ahead of release 6, which writes them (ADR 0539 clauses 2 and 6).
{State: "the compositions' digests", Writer: "controller (lease holder), compare-and-set, from release 6; the operator's " +
"account on the hand path alone (ADR 0539 clauses 2 and 10)",
KeptIn: "key-value " + ControllerComposedBucket, Others: "the assigner reads them",
Subjects: kvOf(ControllerComposedBucket), Writes: isControllerOrTheHandPath},
{State: "conditions", Writer: "controller", KeptIn: "key-value " + ConditionsBucket + " (and its history, " +
ConditionHistoryBucket + "); how often each was observed, when last and its newest evidence in the " +
"controller's store, so a look that changes nothing writes no bucket", Others: "raise or clear only through observations the controller reads",
@@ -232,11 +237,12 @@ var WritersTable = []WriterRow{
Others: "read through `reads` as node-engine.have", Subjects: kvOf(delivery.HaveBucket), Writes: ownNodeKeys},
// The contents of a declaration's files over 64 KiB (novox/hq ADR 0486 §2): the controller alone writes the bucket, each
// object under the digest of its content; a node-engine reads one by its name, with no list and no write.
{State: "a declaration's large files", Writer: "controller, each object under the sha256 digest of its content",
{State: "a declaration's large files", Writer: "controller, each object under the sha256 digest of its content; the " +
"operator's account on release 6's hand path alone (ADR 0539 clause 10)",
KeptIn: "the object bucket " + DeclarationFilesBucket + "-<node> of each node, an object removed a day after the " +
"node's latest declaration no longer names it",
Others: "a node-engine reads an object of its own bucket by its name and may neither list the bucket nor write it",
Subjects: []string{"$O.*.>"}, Writes: isController},
Subjects: []string{"$O.*.>"}, Writes: isControllerOrTheHandPath},
{State: "the declared-config mirror", Writer: "controller (lease holder), from its store, only on a change",
KeptIn: "key-value " + DeclaredConfigBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredConfig,
Subjects: kvOf(DeclaredConfigBucket), Writes: isController},
@@ -299,3 +305,9 @@ func SubjectsOverlap(a, b string) bool {
}
}
}
// isControllerOrTheHandPath is the controller, or the operator's account (a person who may invoke every tool) writing
// release 6's hand path: the one second writer ADR 0539 clauses 2 and 10 name, for a composition made by hand.
func isControllerOrTheHandPath(p Principal, pattern string) bool {
return isController(p, pattern) || (p.Kind == KindPerson && administers(p.Invokes))
}
+2
View File
@@ -21,6 +21,8 @@ var designRows = []string{
"a machine's applied state and its report",
"the controller lease",
"plans and their tiers",
// Release 6's compositions' digests, granted ahead of it (ADR 0539 clauses 2 and 6, issue 1945).
"the compositions' digests",
"conditions",
"calls and their outcomes",
"the hand-act log",
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the repository's root files or other directories): mesh-build-go never keeps this package's verdict (issue 1935).
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the repository's root files or other directories): mesh-build-go never keeps this package's verdict (issue 1935).
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the repository's root files or other directories): mesh-build-go never keeps this package's verdict (issue 1935).
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
-1
View File
@@ -1 +0,0 @@
These tests read files outside this directory (the repository's root files or other directories): mesh-build-go never keeps this package's verdict (issue 1935).