Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd8ea8ad4f | ||
|
|
1350c428ed | ||
|
|
7f8e5a821c | ||
|
|
3ae2d6e17c | ||
|
|
34b105ea88 | ||
|
|
44d9d40582 | ||
|
|
74bed5a800 |
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 +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).
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 +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 +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 +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 +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 +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 +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 +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).
|
||||
Reference in New Issue
Block a user