Every module follows the assigner's new places: the contracts' go/ path, mesh-wants in the delivery contracts, the state and tool assignments (issue 1966)

The legacy key form placement.<node> is gone from the client, so the readers and their fixtures lose it. The check noplacement
now also refuses the assigner's retired names in a manifest.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:29:07 +01:00
parent 2dc9e07e88
commit c9d8950fd0
109 changed files with 682 additions and 681 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ func TestRefusalsJudgeATree(t *testing.T) {
{"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""},
{"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""},
{"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""},
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-module-assigner.placement"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-module-assigner.assignments"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
+9 -5
View File
@@ -8,7 +8,8 @@
// 2. a `particulars` file at the root (the mesh's node names come from the checker's run, §16);
//
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`;
// or `mesh-delivery-indexer_placement…`), or the assigner's retired state placement (`mesh-delivery-assigner.placement…`,
// `mesh-module-assigner.placement…`, its views and its read tool), in any string value, such as `reads`;
//
// The controller's assignments, which item 4 once listed readers of, are refused with every other name of the
// controller by checks/controllerdeps (novox/hq ADR 0590 §5).
@@ -27,8 +28,11 @@ import (
"strings"
)
// Prefixes are the names of the indexer's placement parts; a manifest string starting with one names them.
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement"}
// Prefixes are the names of the indexer's placement parts and the retired names of the assigner's state, now
// assignments (novox/hq ADR 0641); a manifest string starting with one names them.
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement",
"mesh-delivery-assigner.placement", "mesh-delivery-assigner_placement", "mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.assigner_read_placement", "mesh-module-assigner.placement"}
// Findings is every refusal in the catalogue at root, sorted.
func Findings(root string) ([]string, error) {
@@ -55,7 +59,7 @@ func Findings(root string) ([]string, error) {
}
module := filepath.Base(filepath.Dir(file))
for _, s := range named(doc, nil) {
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-module-assigner's (novox/hq ADR 0567 §12)", module, s))
out = append(out, fmt.Sprintf("%s: names %q, a name that is retired: the indexer keeps no placement (novox/hq ADR 0567 §12) and the seat mesh-module-assigner's state is assignments (novox/hq ADR 0641)", module, s))
}
}
sort.Strings(out)
@@ -99,5 +103,5 @@ func main() {
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no placement directory, and no manifest reads the indexer's placement")
fmt.Println("no placement directory, and no manifest names the retired placement")
}
+6 -2
View File
@@ -13,13 +13,17 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
files map[string]string
want string // empty: clean
}{
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-assigner.placement"]}`}, ""},
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-assigner.assignments"]}`}, ""},
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-module-assigner.placement"]}`}, ""},
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-module-assigner.assignments"]}`}, ""},
{"the assigner's retired state", map[string]string{"modules/e/module.json": `{"module":"e","reads":["mesh-delivery-assigner.placement"]}`}, `e: names "mesh-delivery-assigner.placement"`},
{"its retired views family", map[string]string{"modules/f/module.json": `{"module":"f","consumes":["mesh-delivery-assigner.views.placement"]}`}, "views.placement"},
{"its retired read tool", map[string]string{"modules/g/module.json": `{"module":"g","invokes":["mesh-delivery-assigner.assigner_read_placement"]}`}, "assigner_read_placement"},
{"the seat's retired verb", map[string]string{"modules/h/module.json": `{"module":"h","invokes":["mesh-module-assigner.placement"]}`}, "mesh-module-assigner.placement"},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
+1 -1
View File
@@ -109,7 +109,7 @@ func TestCheckAddressesRefusesTheControllersAddresses(t *testing.T) {
{"`mesh-controller.<verb>`", true},
{"the mesh-controller module is gone.", false},
{"located in mesh-controller#246", false},
{"ask mesh-module-assigner.placement", false},
{"ask mesh-module-assigner.assignments", false},
} {
got := checkAddresses([]text{{file: "m/a.go", line: 1, s: c.s}})
if (len(got) == 1) != c.want {
@@ -20,9 +20,9 @@ import (
"fmt"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"sort"
"strings"
@@ -6,9 +6,9 @@ import (
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"slices"
"sort"
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
+1 -1
View File
@@ -122,7 +122,7 @@ The agent kept reaching for `ssh <machine> journalctl` while the service manager
- **The "instead of" table.** Every seat verb and module tool may say which shell commands it replaces
(`replaces`, in the seat's definition or the module's manifest). The module reads the bus — the seats' verbs from the
seats' contract, the placed modules' manifests from `mesh-delivery-indexer.modules` and `.manifests`, the
machines and where each module runs from `mesh-delivery-assigner.placement` (issue 1126: the controller that
machines and where each module runs from `mesh-delivery-assigner.assignments` (issue 1126: the controller that
answered this is not placed) — at start, every ten minutes and on `claude_code_render`, keeps it in its
state (`mesh-tools.json`), and renders it into `CLAUDE.md` as one row per seat or module: `<node>/<seat>.` and
each verb with the commands it replaces. Generated, never written by hand: a verb that gains `replaces` is in
@@ -277,9 +277,9 @@ func TestTheManifestListsEveryToolServed(t *testing.T) {
_ = json.Unmarshal(raw, &m)
// The hand-act seat's warranted and hand-act, the seat mesh-catalog's seat list (ADR 0635 §1), and the owners' read tools of the three followed states; no verb of
// the controller, and no read of the indexer's or the assigner's state (ADR 0615 §1): their views are consumed.
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.seat-definitions", ModulesTool, ManifestsTool, PlacementTool}) ||
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.seat-definitions", ModulesTool, ManifestsTool, AssignmentsTool}) ||
!slices.Equal(m.Reads, []string{"claude-licence-manager.bindings"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests", "mesh-delivery-assigner.views.placement", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed"}) {
!slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests", "mesh-delivery-assigner.views.assignments", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed"}) {
t.Errorf("invokes = %v, reads = %v, consumes = %v", m.Invokes, m.Reads, m.Consumes)
}
if src, _ := os.ReadFile("proposals.go"); !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) ||
@@ -17,7 +17,7 @@ import (
func meshForTheGuard() GuardData {
defined := []seatdef.Definition{
{Seat: "mesh-module-assigner", Scope: "mesh", Verbs: []seatdef.Verb{
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
{Name: "assignments", Description: "Read assignments: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
{Seat: "node-service-manager", Scope: "node", Verbs: []seatdef.Verb{
{Name: "status", Description: "One unit as the service manager sees it now: its states.", Replaces: []string{"systemctl status", "systemctl is-active"}},
{Name: "restart", Description: "Restart one unit.", Replaces: []string{"systemctl restart"}},
@@ -80,14 +80,14 @@ func TestSSHToAMeshMachineIsRefusedNamingTheTool(t *testing.T) {
"ssh -o StrictHostKeyChecking=no 10.10.0.1 docker ps -a": "anchor/docker.docker_list",
"ssh anchor docker logs --tail 100 nats": "anchor/docker.docker_logs",
"ssh laptop docker logs x": "anchor/docker.docker_logs",
"ssh a uptime": "mesh-module-assigner.placement",
"ssh a uptime": "mesh-module-assigner.assignments",
"cd /tmp && ssh anchor journalctl": "anchor/node-service-manager.journal",
"sudo -u root ssh anchor journalctl": "anchor/node-service-manager.journal",
"timeout 10 ssh anchor journalctl": "anchor/node-service-manager.journal",
"env LANG=C ssh anchor journalctl": "anchor/node-service-manager.journal",
"bash -c 'ssh anchor journalctl -u x'": "anchor/node-service-manager.journal",
`echo "$(ssh anchor journalctl -n 5)" | tail`: "anchor/node-service-manager.journal",
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-module-assigner.placement",
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-module-assigner.assignments",
"ssh -J anchor 203.0.113.9 journalctl": "anchor/node-service-manager.journal",
"ssh via-anchor journalctl": "anchor/node-service-manager.journal",
"ssh nas.lan journalctl": "anchor/node-service-manager.journal",
@@ -6,8 +6,8 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"os"
@@ -44,10 +44,10 @@ func (b aBus) Get(read, key string) ([]byte, bool, error) {
// someSeats are two seats as the seats' contract says them: one held once for the mesh, one on every machine.
func someSeats() []seatdef.Definition {
return []seatdef.Definition{
{Seat: "mesh-module-assigner", Domain: "Placement", Scope: "mesh", Verbs: []seatdef.Verb{
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}},
{Seat: "mesh-module-assigner", Domain: "Core", Scope: "mesh", Verbs: []seatdef.Verb{
{Name: "assignments", Description: "Read assignments: one node's key.", Replaces: []string{"hostnamectl", "uptime"}},
{Name: "propose", Description: "Propose a change."}}},
{Seat: "node-service-manager", Domain: "Placement", Scope: "node", Verbs: []seatdef.Verb{
{Seat: "node-service-manager", Domain: "Core", Scope: "node", Verbs: []seatdef.Verb{
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot.", Replaces: []string{"systemctl status", "systemctl is-active"}},
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
}
@@ -57,18 +57,18 @@ func someSeats() []seatdef.Definition {
// saying what its tool replaces, as the seat mesh-module-assigner and the indexer keep them on the bus.
func aMesh(t *testing.T) aBus {
t.Helper()
b := aBus{PlacementRead: {}, ModulesRead: {}, ManifestsRead: {}}
for _, p := range []mesh.Placement{
b := aBus{AssignmentsRead: {}, ModulesRead: {}, ManifestsRead: {}}
for _, p := range []mesh.Assignments{
{V: 1, Node: "anchor", Decided: mesh.NodeDecided{PublicDomain: "Anchor.Example"},
Modules: []mesh.PlacedModule{{Module: "docker"}, {Module: "zsh"}}},
{V: 1, Node: "laptop"},
} {
values, err := assignerclient.PlacementValues(p)
values, err := assignerclient.AssignmentsValues(p)
if err != nil {
t.Fatal(err)
}
for k, v := range values {
b[PlacementRead][k] = v
b[AssignmentsRead][k] = v
}
}
for module, manifest := range map[string]string{
@@ -105,7 +105,7 @@ func TestTheMeshsToolsAreReadFromTheBus(t *testing.T) {
for _, r := range m.Replacements {
addresses = append(addresses, r.Address)
}
if got := strings.Join(addresses, ","); got != "mesh-module-assigner.placement,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
if got := strings.Join(addresses, ","); got != "mesh-module-assigner.assignments,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
t.Errorf("replacements: %s", got)
}
t.Run("the again tool answers the operator and the healers only", func(t *testing.T) {
@@ -138,7 +138,7 @@ func TestTheMeshsToolsAreReadFromTheBus(t *testing.T) {
addrs = append(addrs, r.Address)
}
// The foundation's bootstrapped seats are held beside the two the seat answered, in reading order.
if err != nil || !slices.Contains(addrs, "mesh-module-assigner.placement") || !slices.Contains(addrs, "node-service-manager.journal") {
if err != nil || !slices.Contains(addrs, "mesh-module-assigner.assignments") || !slices.Contains(addrs, "node-service-manager.journal") {
t.Fatalf("replacements from the bus: %v, %v", addrs, err)
}
})
@@ -159,7 +159,7 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
for _, want := range []string{
"| `<node>/node-service-manager.` | `status` — `systemctl status`, `systemctl is-active` · `journal` — `journalctl` |",
"| `<node>/docker.` | `docker_logs` — `docker logs` |",
"| `mesh-module-assigner.` | `placement` — `hostnamectl`, `uptime` |",
"| `mesh-module-assigner.` | `assignments` — `hostnamectl`, `uptime` |",
"Never `ssh` to a mesh machine",
} {
if !strings.Contains(table, want) {
@@ -171,7 +171,7 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
t.Errorf("not the machines' seats, then modules, then the mesh's:\n%s", table)
}
// What the guard here named most comes first: its row, and in its row its verb.
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-module-assigner.placement": 1}
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-module-assigner.assignments": 1}
ranked := InsteadOf(&m)
if !strings.Contains(ranked, "| `<node>/node-service-manager.` | `journal` — `journalctl` · `status`") ||
strings.Index(ranked, "`mesh-module-assigner.`") > strings.Index(ranked, "`<node>/docker.`") {
@@ -221,8 +221,8 @@ func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
}
}
// Where a module runs is read from the seat mesh-module-assigner's placement (issue 1126 B13), never the controller's answer.
func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
// Where a module runs is read from the seat mesh-module-assigner's assignments (issue 1126 B13), never the controller's answer.
func TestTheMachinesRunningAModuleAreReadFromAssignments(t *testing.T) {
b := aMesh(t)
if got, err := NodesRunning(b, "zsh"); err != nil || strings.Join(got, ",") != "anchor" {
t.Errorf("zsh runs on %v (%v)", got, err)
@@ -231,17 +231,17 @@ func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
t.Errorf("a module that runs nowhere: %v (%v)", got, err)
}
if _, err := NodesRunning(aBus{}, "zsh"); err == nil {
t.Error("an unreadable placement was read as no machine")
t.Error("an unreadable assignments was read as no machine")
}
}
// ADR 0615 §1: the tools and the placement come from the owners' first views and their views events, never their state.
// ADR 0615 §1: the tools and the assignments come from the owners' first views and their views events, never their state.
// A reader whose first views are answered from the fixture reads what the state read gave, and a views event changes
// what it reads (CAT-CC3).
func TestTheMeshIsReadFromTheOwnersFirstViews(t *testing.T) {
bus := aMesh(t)
r := newMeshViews()
for _, name := range []string{ModulesRead, ManifestsRead, PlacementRead} {
for _, name := range []string{ModulesRead, ManifestsRead, AssignmentsRead} {
state := strings.TrimPrefix(name, strings.Split(name, ".")[0]+".")
values := map[string]json.RawMessage{}
for k, v := range bus[name] {
@@ -9,5 +9,5 @@ func instructionsText(node, role string) string {
node +
"`\n- **Role:** " +
role +
"\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-module-assigner.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
"\n- The other nodes, their roles and what runs where: ask the seat `mesh-module-assigner` (`mesh-module-assigner.assignments`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
}
+6 -6
View File
@@ -209,7 +209,7 @@ func (s stateOf) Get(key string) (json.RawMessage, bool, error) {
return e.Value, true, nil
}
// nodesRunningMe is the nodes claude-code runs on, from the assigner's placement — for the register tool's question.
// nodesRunningMe is the nodes claude-code runs on, from the assigner's assignments — for the register tool's question.
func nodesRunningMe() ([]string, error) { return NodesRunning(viewStates{meshViews}, "claude-code") }
// The owners' read tools of the states this module follows, which the manifest invokes (novox/hq ADR 0615 §1).
@@ -221,12 +221,12 @@ const (
)
const (
ModulesTool = indexerModule + ".indexer_read_modules"
ManifestsTool = indexerModule + ".indexer_read_manifests"
PlacementTool = assignerModule + ".assigner_read_placement"
ModulesTool = indexerModule + ".indexer_read_modules"
ManifestsTool = indexerModule + ".indexer_read_manifests"
AssignmentsTool = assignerModule + ".assigner_read_assignments"
)
// meshViews is this module's picture of the indexer's modules and manifests and the assigner's placement: each asked
// meshViews is this module's picture of the indexer's modules and manifests and the assigner's assignments: each asked
// once of the owner's read tool, then followed on the owner's views family. It reads none of their state.
var meshViews *views.Reader
@@ -245,7 +245,7 @@ func newMeshViews() *views.Reader {
}}
v.Follow(indexerModule, "modules", ModulesTool)
v.Follow(indexerModule, "manifests", ManifestsTool)
v.Follow(assignerModule, "placement", PlacementTool)
v.Follow(assignerModule, "assignments", AssignmentsTool)
return v
}
+20 -20
View File
@@ -7,7 +7,7 @@ package main
// Read from the bus at start and every few minutes (issue 1126 B13; the controller that answered this before is
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-contracts), the modules'
// own tools from the manifests the indexer keeps (`mesh-delivery-indexer.modules` and `.manifests`), and the
// machines and where each module runs from the seat mesh-module-assigner's `placement`. Kept in the module's state so
// machines and where each module runs from the seat mesh-module-assigner's `assignments`. Kept in the module's state so
// a render never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb
// that gains a `replaces` is in the next render's table and the guard's next refusal.
@@ -16,8 +16,8 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"net"
@@ -92,9 +92,9 @@ func ReadMeshTools(p Paths) *MeshTools {
// The states read, each declared in the manifest's reads.
const (
PlacementRead = (assignerModule + "." + mesh.StatePlacement)
ModulesRead = indexerModule + "." + catalog.StateModules
ManifestsRead = indexerModule + "." + catalog.StateManifests
AssignmentsRead = (assignerModule + "." + mesh.StateAssignments)
ModulesRead = indexerModule + "." + catalog.StateModules
ManifestsRead = indexerModule + "." + catalog.StateManifests
)
// States reads the module states the manifest declares in its reads: a state's keys, and one key's value.
@@ -110,17 +110,17 @@ type ModuleTools struct {
Replaces map[string][]string
}
// placements is every node's placement, from the seat mesh-module-assigner's state.
func placements(st States) (map[string]mesh.Placement, error) {
keys, err := st.Keys(PlacementRead)
// assignments is every node's assignments, from the seat mesh-module-assigner's state.
func assignments(st States) (map[string]mesh.Assignments, error) {
keys, err := st.Keys(AssignmentsRead)
if err != nil {
return nil, fmt.Errorf("%s: %w", PlacementRead, err)
return nil, fmt.Errorf("%s: %w", AssignmentsRead, err)
}
read := assignerclient.NewKeys()
for _, k := range keys {
v, found, err := st.Get(PlacementRead, k)
v, found, err := st.Get(AssignmentsRead, k)
if err != nil {
return nil, fmt.Errorf("%s %s: %w", PlacementRead, k, err)
return nil, fmt.Errorf("%s %s: %w", AssignmentsRead, k, err)
}
if found {
_, _ = read.Put(k, v, 0, false) // a key that does not read places nothing
@@ -129,8 +129,8 @@ func placements(st States) (map[string]mesh.Placement, error) {
return read.All(), nil
}
// runsOn is, by module, the nodes placement puts it on, sorted.
func runsOn(placed map[string]mesh.Placement) map[string][]string {
// runsOn is, by module, the nodes assignments puts it on, sorted.
func runsOn(placed map[string]mesh.Assignments) map[string][]string {
out := map[string][]string{}
for node, p := range placed {
for _, m := range p.Modules {
@@ -143,9 +143,9 @@ func runsOn(placed map[string]mesh.Placement) map[string][]string {
return out
}
// NodesRunning is the nodes placement puts module on.
// NodesRunning is the nodes assignments puts module on.
func NodesRunning(st States, module string) ([]string, error) {
placed, err := placements(st)
placed, err := assignments(st)
if err != nil {
return nil, err
}
@@ -261,11 +261,11 @@ func lookup(name string) []string {
return addrs
}
// ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node placement names
// ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node assignments names
// with its domains and addresses. A part that cannot be read is left out and said in the error; what was read is kept.
func ReadMeshToolsFrom(st States, defined []seatdef.Definition, resolve func(string) []string) (MeshTools, error) {
var m MeshTools
placed, err := placements(st)
placed, err := assignments(st)
if err != nil {
m.Replacements = ReplacementsOf(defined, nil)
return m, err
@@ -340,7 +340,7 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
}
// MeshToolsEvery is how often the machines' addresses are resolved again; the tools and the placement are read again
// MeshToolsEvery is how often the machines' addresses are resolved again; the tools and the assignments are read again
// when their followed states change (a views event), not on this timer.
const MeshToolsEvery = 10 * time.Minute
@@ -353,7 +353,7 @@ func refreshMeshTools(p Paths) {
select {
case <-meshViews.Ready():
case <-time.After(MeshToolsFirst):
say("the first views of the tools and the placement were not answered in %s; reading what is held", MeshToolsFirst)
say("the first views of the tools and the assignments were not answered in %s; reading what is held", MeshToolsFirst)
}
startSeatList(seatlist.StdioBus())
for {
@@ -32,11 +32,11 @@
"withKey": {
"managed-mcp.json": "{\n \"mcpServers\": {\n \"anton\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"/a/b.js\"\n ],\n \"env\": {}\n },\n \"docs\": {\n \"type\": \"stdio\",\n \"command\": \"docs-mcp\",\n \"args\": [\n \"--x\"\n ]\n },\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n },\n \"search\": {\n \"type\": \"http\",\n \"url\": \"https://s.example/mcp\"\n }\n }\n}\n",
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true,\n \"apiKeyHelper\": \"/state/api-key-helper\"\n}\n",
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-module-assigner.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the seat `mesh-module-assigner` (`mesh-module-assigner.assignments`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
},
"plain": {
"managed-mcp.json": "{\n \"mcpServers\": {\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n }\n }\n}\n",
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true\n}\n",
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-module-assigner.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the seat `mesh-module-assigner` (`mesh-module-assigner.assignments`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
}
}
+2 -2
View File
@@ -7,8 +7,8 @@ require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
+2 -2
View File
@@ -28,12 +28,12 @@
"seat:mesh-catalog.seat-definitions",
"mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-assigner.assigner_read_placement"
"mesh-delivery-assigner.assigner_read_assignments"
],
"consumes": [
"mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.manifests",
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.assignments",
"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed"
],
+1 -1
View File
@@ -3,7 +3,7 @@ package main
// Where disk-load learns the data items it measures (novox/hq ADR 0577 clause 2): the node-engine's state data
// (engineDataBucket), one key `<node>.<module>` per module placed on this node that declares data items, as the
// engine composed them. disk-load watches its own node's keys and composes nothing: it reads no resolved values, no
// manifests and no placement, so a manifest its mesh-sdk cannot judge never silences it, and a round reads one watched
// manifests and no assignments, so a manifest its mesh-sdk cannot judge never silences it, and a round reads one watched
// map instead of several hundred keys over the tunnel (issue 1955).
import (
@@ -11,7 +11,7 @@ import (
// CAT-BI1 (design 51 *Publishing*): the module publishes and copies through the image seat's verbs, which its manifest
// invokes, hears the seat's outcomes through its consumes, and holds no registry: no registry file, no artifact-store
// requirement, and none of its files names the store's placement or binds it (issue 1703: the module is moved, and only
// requirement, and none of its files names the store's assignment or binds it (issue 1703: the module is moved, and only
// what the mesh fills reaches its files).
func TestTheModuleInvokesTheImageSeatAndHoldsNoRegistry(t *testing.T) {
var m struct {
+1 -1
View File
@@ -33,7 +33,7 @@ module node-tools serves on loopback (novox/hq ADR 0152: whoever is on the machi
puts the call on the bus, as every agent session on the machine does. The console's address is read from
`/etc/mesh-cli/console`, or `MESH_CONSOLE` when set, or node-tools' declared port when the file is not there yet.
- **Who judges.** The seat that owns the job: a change of placement is a proposal the operator approves
- **Who judges.** The seat that owns the job: a change of assignments is a proposal the operator approves
(`mesh-module-assigner.propose`), a config value is set with its why (`mesh-config.config`), a silence or a hand act is
recorded as one. Until novox/hq issue 1126 the controller ran these lines and judged the account (ADR 0272); with
the controller unassigned, that judgement went with it.
+18 -18
View File
@@ -111,17 +111,17 @@ var commands = []command{
// What runs where.
{words: []string{"status"}, does: "what is wrong, what is quiet, what is moving (the delivery view)",
build: fixed("mesh-delivery-view.view_status")},
{words: []string{"nodes"}, does: "the nodes placement holds", build: fixed("mesh-module-assigner.placement"),
{words: []string{"nodes"}, does: "the nodes assignments holds", build: fixed("mesh-module-assigner.assignments"),
render: renderNodes},
{words: []string{"node", "list"}, does: "the nodes placement holds", build: fixed("mesh-module-assigner.placement"),
{words: []string{"node", "list"}, does: "the nodes assignments holds", build: fixed("mesh-module-assigner.assignments"),
render: renderNodes},
{words: []string{"node", "show"}, usage: "<node>", does: "one node's placement: what it runs and its decided fields",
build: nodeArg("mesh-module-assigner.placement")},
{words: []string{"node", "show"}, usage: "<node>", does: "one node's assignments: what it runs and its decided fields",
build: nodeArg("mesh-module-assigner.assignments")},
{words: []string{"node", "public-domain"}, usage: "<node> [<domain> | --clear] [--why <text>]",
does: "the domain a node composes its routed names under; with a domain or --clear, proposed to placement",
does: "the domain a node composes its routed names under; with a domain or --clear, proposed to assignments",
allowed: []string{"clear", "why"}, build: publicDomain},
{words: []string{"node", "add"}, instead: "a new machine joins through the seat mesh-node-enroller: nox enrol issue <node> --why <text>"},
{words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-module-assigner.placement shows what a node decides"},
{words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-module-assigner.assignments shows what a node decides"},
{words: []string{"plan"}, usage: "<node>", does: "what the assigner wants the node to run", build: optNode("mesh-delivery-view.assigner_wants")},
{words: []string{"haves"}, usage: "[<node>]", does: "what each node runs, as its node-engine said", build: optNode("mesh-delivery-view.node_haves")},
{words: []string{"switch"}, does: "what each node runs, as its node-engine said", build: fixed("mesh-delivery-view.node_haves")},
@@ -133,7 +133,7 @@ var commands = []command{
return call{l.pos[0] + "/node-engine.composition", map[string]any{"module": l.pos[1]}}, nil
}},
{words: []string{"seats"}, does: "who holds each seat, and where", build: func(line) (call, error) {
return call{"mesh-module-assigner.placement", map[string]any{"seats": "true"}}, nil
return call{"mesh-module-assigner.assignments", map[string]any{"seats": "true"}}, nil
}},
{words: []string{"seat"}, instead: "a seat is held by placing its module: nox assign <node> <module> --why <text> (mesh-module-assigner.propose)"},
{words: []string{"assign"}, usage: "<node> <module>... --why <text>", does: "propose placing modules on a node",
@@ -155,9 +155,9 @@ var commands = []command{
}
return propose(l, map[string]any{"op": "unpin", "node": l.pos[0], "provision": l.pos[1]})
}},
{words: []string{"proposals"}, usage: "[<id>]", does: "the open proposals of placement", build: optArg("mesh-module-assigner.proposals", "id")},
{words: []string{"overlay", "show"}, does: "every node's placement, with its tunnel", build: fixed("mesh-module-assigner.placement")},
{words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its placement: mesh-module-assigner.propose with op decide"},
{words: []string{"proposals"}, usage: "[<id>]", does: "the open proposals of assignments", build: optArg("mesh-module-assigner.proposals", "id")},
{words: []string{"overlay", "show"}, does: "every node's assignments, with its tunnel", build: fixed("mesh-module-assigner.assignments")},
{words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its assignments: mesh-module-assigner.propose with op decide"},
// Deliveries and builds.
{words: []string{"deliveries"}, usage: "[--state s] [--repository owner/repo] [--group g] [--all]",
@@ -621,10 +621,10 @@ func numbers(a map[string]any, names ...string) (map[string]any, error) {
return a, nil
}
// propose is a change of placement with its why.
// propose is a change of assignments with its why.
func propose(l line, changes ...map[string]any) (call, error) {
if !l.has("why") {
return call{}, usageErr("a change of placement needs --why; the operator is asked to approve it")
return call{}, usageErr("a change of assignments needs --why; the operator is asked to approve it")
}
list := make([]any, len(changes))
for i, c := range changes {
@@ -649,7 +649,7 @@ func placeModules(op string) func(line) (call, error) {
func publicDomain(l line) (call, error) {
switch {
case len(l.pos) == 1 && !l.has("clear"):
return call{"mesh-module-assigner.placement", map[string]any{"node": l.pos[0]}}, nil
return call{"mesh-module-assigner.assignments", map[string]any{"node": l.pos[0]}}, nil
case len(l.pos) == 1:
return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": nil})
case len(l.pos) == 2 && !l.has("clear"):
@@ -718,14 +718,14 @@ func addressCall(address string, rest []string) (call, error) {
return call{address, a}, nil
}
// renderNodes shapes placement's keys as one line per node: its name, address and site.
// renderNodes shapes the assignments' keys as one line per node: its name, address and site.
func renderNodes(text string) string {
var p struct {
Keys map[string]struct {
Placement struct {
Assignments struct {
Decided map[string]any `json:"decided"`
Modules []any `json:"modules"`
} `json:"placement"`
} `json:"assignments"`
} `json:"keys"`
}
// The console says after the answer which machine answered it ("answered by ace"): kept, below the lines.
@@ -745,8 +745,8 @@ func renderNodes(text string) string {
var b strings.Builder
for _, n := range names {
k := p.Keys[n]
fmt.Fprintf(&b, "%-10s %-15v %-8v %d modules\n", n, orDash(k.Placement.Decided["address"]),
orDash(k.Placement.Decided["site"]), len(k.Placement.Modules))
fmt.Fprintf(&b, "%-10s %-15v %-8v %d modules\n", n, orDash(k.Assignments.Decided["address"]),
orDash(k.Assignments.Decided["site"]), len(k.Assignments.Modules))
}
if after != "" {
b.WriteString(after + "\n")
+6 -6
View File
@@ -87,13 +87,13 @@ func TestEachLineCallsItsOwner(t *testing.T) {
address string
args string
}{
{"node show ace", "mesh-module-assigner.placement", `{"node":"ace"}`},
{"node public-domain ace", "mesh-module-assigner.placement", `{"node":"ace"}`},
{"node show ace", "mesh-module-assigner.assignments", `{"node":"ace"}`},
{"node public-domain ace", "mesh-module-assigner.assignments", `{"node":"ace"}`},
{"node public-domain ace example.org --why moved", "mesh-module-assigner.propose", `{"changes":[{"field":"public-domain","node":"ace","op":"decide","value":"example.org"}],"why":"moved"}`},
{"plan ace", "mesh-delivery-view.assigner_wants", `{"node":"ace"}`},
{"haves", "mesh-delivery-view.node_haves", `{}`},
{"composition ace plex", "ace/node-engine.composition", `{"module":"plex"}`},
{"seats", "mesh-module-assigner.placement", `{"seats":"true"}`},
{"seats", "mesh-module-assigner.assignments", `{"seats":"true"}`},
{"assign ace plex sonarr --why=media", "mesh-module-assigner.propose", `{"changes":[{"module":"plex","node":"ace","op":"place"},{"module":"sonarr","node":"ace","op":"place"}],"why":"media"}`},
{"unassign ace plex --why gone", "mesh-module-assigner.propose", `{"changes":[{"module":"plex","node":"ace","op":"take-off"}],"why":"gone"}`},
{"pin ace postgres-database novox postgres --why one", "mesh-module-assigner.propose", `{"changes":[{"node":"ace","op":"pin","provider":{"module":"postgres","node":"novox"},"provision":"postgres-database"}],"why":"one"}`},
@@ -194,9 +194,9 @@ func TestAnswersRefusalsAndSilenceAreSaid(t *testing.T) {
if code, out, errOut := runLine("enrol", "tokens"); code != 1 || out != "" || !strings.Contains(errOut, "nothing serves") {
t.Errorf("an error answer: exit %d, stdout %q, stderr %q", code, out, errOut)
}
placement := `{"keys":{"novox":{"placement":{"decided":{"address":"10.10.0.1","site":"hub"},"modules":[{},{}]}},` +
`"ace":{"placement":{"decided":{"address":"10.10.0.2"},"modules":[{}]}}}}`
console(t, map[string]string{"mesh-module-assigner.placement": placement})
assignments := `{"keys":{"novox":{"assignments":{"decided":{"address":"10.10.0.1","site":"hub"},"modules":[{},{}]}},` +
`"ace":{"assignments":{"decided":{"address":"10.10.0.2"},"modules":[{}]}}}}`
console(t, map[string]string{"mesh-module-assigner.assignments": assignments})
if code, out, _ := runLine("nodes"); code != 0 || !strings.HasPrefix(out, "ace ") ||
!strings.Contains(out, "10.10.0.1") || !strings.Contains(out, "2 modules") || !strings.HasSuffix(out, "answered by ace\n") {
t.Errorf("nodes: exit %d, %q", code, out)
@@ -19,7 +19,7 @@ import (
"time"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/motion"
@@ -49,7 +49,7 @@ func (k *Keeper) aboutOf(kind, source string, s seat.Subject) []about {
out = append(out, about{Node: parts[1], Module: parts[0]})
}
case cc.ScopeSeat:
// The module the placement says holds the seat on that node, or the seat's own name where none is read.
// The module the assignments says holds the seat on that node, or the seat's own name where none is read.
if len(parts) >= 2 {
node, seat := parts[len(parts)-1], strings.Join(parts[:len(parts)-1], ".")
module := k.holders[node+"."+seat]
@@ -200,25 +200,25 @@ func (k *Keeper) Have(key string, value []byte, deleted bool) error {
return k.judge(node, module, "")
}
// Placement takes one change of the placement: which module holds which seat on a node. The keys are read in either
// form, a node key and its module keys or the form before (delivery.PlacementKeys; research 072 row 37).
func (k *Keeper) Placement(key string, value []byte, deleted bool) {
// Assignments takes one change of the assignments: which module holds which seat on a node. The keys are a node key and
// its module keys (assignerclient.Keys; research 072 row 37).
func (k *Keeper) Assignments(key string, value []byte, deleted bool) {
k.mu.Lock()
defer k.mu.Unlock()
k.init()
node, err := k.placement.Put(key, value, 0, deleted)
node, err := k.assignments.Put(key, value, 0, deleted)
if node == "" {
return
}
if err != nil {
k.Logf("the placement of %s is not readable as it stands, so its seats' holders are kept as last read: %v", node, err)
k.Logf("the assignments of %s is not readable as it stands, so its seats' holders are kept as last read: %v", node, err)
}
for hk := range k.holders {
if strings.HasPrefix(hk, node+".") {
delete(k.holders, hk)
}
}
p, _, ok := k.placement.Of(node)
p, _, ok := k.assignments.Of(node)
if !ok {
return
}
@@ -5,12 +5,12 @@ import (
"strings"
"testing"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/motion"
)
@@ -98,22 +98,17 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
t.Errorf("%s: something is still held: %+v", c.name, r.k.Held())
}
}
// A seat's finding is about the module the placement says holds the seat on that node (the lead, 2026-10-29): held
// A seat's finding is about the module the assignments says holds the seat on that node (the lead, 2026-10-29): held
// while the assigner is delivered on ace, though the seat is named mesh-module-assigner.
r := newRig()
// Research 072 row 37: the placement in its keys per module and node; a stale legacy key beside them is not read.
stale, err := assignerclient.EncodePlacement(mesh.Placement{Node: "ace"})
if err != nil {
t.Fatal(err)
}
r.k.Placement("placement.ace", stale, false)
placed, err := assignerclient.PlacementValues(mesh.Placement{Node: "ace", Modules: []mesh.PlacedModule{{Module: "mesh-delivery-assigner"}},
// Research 072 row 37: the assignments in its keys per module and node.
placed, err := assignerclient.AssignmentsValues(mesh.Assignments{Node: "ace", Modules: []mesh.PlacedModule{{Module: "mesh-delivery-assigner"}},
Holds: []mesh.Holding{{Claim: "mesh-module-assigner", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
if err != nil {
t.Fatal(err)
}
r.k.Placement("ace.mesh-delivery-assigner", placed["ace.mesh-delivery-assigner"], false)
r.k.Placement("ace.node", placed["ace.node"], false)
r.k.Assignments("ace.mesh-delivery-assigner", placed["ace.mesh-delivery-assigner"], false)
r.k.Assignments("ace.node", placed["ace.node"], false)
_ = r.k.Want("want.ace.mesh-delivery-assigner", want(ctrlNext, ctrlNextComp), false)
_ = r.k.Have("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK), false)
seatKey, seatRaw := finding(t, selfCheck, cc.ScopeSeat, "mesh-module-assigner.ace", "holder-silent", "D3", cc.Warning, r.now)
@@ -17,8 +17,8 @@ import (
"sync"
"time"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/motion"
@@ -33,7 +33,7 @@ const (
watchmanModule = "mesh-delivery-watchman"
selfCheckModule = "mesh-self-check"
healersModule = "mesh-healers"
// The holders of the placement and the catalog, whose views and findings the keeper follows (the manifest test holds
// The holders of the assignments and the catalog, whose views and findings the keeper follows (the manifest test holds
// these equal to the manifest's `consumes` and `invokes`).
assignerModule = "mesh-delivery-assigner"
indexerModule = "mesh-delivery-indexer"
@@ -71,9 +71,9 @@ var Observers = []string{watchmanModule, selfCheckModule, healersModule, assigne
const (
AttemptsRead = healersModule + "." + meshhealers.StateAttempts
WantsRead = assignerModule + ".wants"
// PlacementRead is the mesh-module-assigner seat's placement, read for which module holds a seat on a node (hold.go; the
// AssignmentsRead is the mesh-module-assigner seat's assignments, read for which module holds a seat on a node (hold.go; the
// lead, 2026-10-29, for ADR 0564 clause 2's inputs).
PlacementRead = assignerModule + "." + mesh.StatePlacement
AssignmentsRead = assignerModule + "." + mesh.StateAssignments
HavesRead = "node-engine.have"
ConditionWrites = 8
)
@@ -127,12 +127,12 @@ type Keeper struct {
attempts map[string]meshhealers.AttemptsByAttemptValue
wants map[string]motion.Want
haves map[string]motion.Have
// holders are, by `<node>.<seat>`, the module the placement says holds the seat there.
// holders are, by `<node>.<seat>`, the module the assignments says holds the seat there.
holders map[string]string
// placement is the placement's keys as heard, assembled per node.
placement *assignerclient.Keys
stuck map[string]string
held map[string]held
// assignments is the assignments' keys as heard, assembled per node.
assignments *assignerclient.Keys
stuck map[string]string
held map[string]held
// passed are the deliveries whose bound passed, by node and module, at the fingerprint wanted then; outOfHold the
// line a key raised out of a hold says (hold.go).
passed map[string]string
@@ -148,7 +148,7 @@ func (k *Keeper) init() {
k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]motion.Want{}, map[string]motion.Have{},
map[string]string{}, map[string]held{}, map[string]clearedMemo{}
k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{}
k.placement = assignerclient.NewKeys()
k.assignments = assignerclient.NewKeys()
}
if k.Now == nil {
k.Now = time.Now
@@ -133,27 +133,27 @@ func Keep(k *Keeper, v *views.Reader) {
}
return nil
})
watch(k, v, PlacementRead, "", func(c stdio.StateChange) error {
k.Placement(c.Key, c.Value, c.Op != stdio.OpPut)
watch(k, v, AssignmentsRead, "", func(c stdio.StateChange) error {
k.Assignments(c.Key, c.Value, c.Op != stdio.OpPut)
return nil
})
}
// The assigner's read tools of the states the keeper follows, which the manifest invokes (ADR 0615).
const (
WantsTool = assignerModule + ".assigner_read_wants"
PlacementTool = assignerModule + ".assigner_read_placement"
FindingsTool = assignerModule + ".assigner_read_findings"
WantsTool = assignerModule + ".assigner_read_wants"
AssignmentsTool = assignerModule + ".assigner_read_assignments"
FindingsTool = assignerModule + ".assigner_read_findings"
// IndexerFindingsTool is the indexer's read tool of its findings: the indexer's state, too, is asked of its owner.
IndexerFindingsTool = indexerModule + ".indexer_read_findings"
)
// NewViews is the keeper's pictures of the assigner's wants, placement and findings and of the indexer's findings. A first view it cannot get past its
// NewViews is the keeper's pictures of the assigner's wants, assignments and findings and of the indexer's findings. A first view it cannot get past its
// bound is a finding of its own, taken in as any observer's: the keeper raises the condition itself.
func NewViews(k *Keeper) *views.Reader {
v := &views.Reader{Module: Module, Logf: logf, Findings: ownFindings{k}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
v.Follow(assignerModule, "wants", WantsTool)
v.Follow(assignerModule, "placement", PlacementTool)
v.Follow(assignerModule, "assignments", AssignmentsTool)
v.Follow(assignerModule, seat.StateFindings, FindingsTool)
v.Follow(indexerModule, seat.StateFindings, IndexerFindingsTool)
return v
@@ -32,7 +32,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
!slices.Equal(Verbs, catalogue.VerbNames(def.Serves)) {
t.Errorf("module %s runs as %s and claims %+v; the def serves %v", m.Module, m.RunsAs, m.Claims, catalogue.VerbNames(def.Serves))
}
// The assigner's wants, placement and findings, and the indexer's findings, are asked once of their read tools and
// The assigner's wants, assignments and findings, and the indexer's findings, are asked once of their read tools and
// followed by their events, not read (ADR 0615 §1).
reads := []string{HavesRead, AttemptsRead}
for _, o := range Observers {
@@ -40,8 +40,8 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
reads = append(reads, o+"."+seat.StateFindings)
}
}
if !slices.Equal(m.Invokes, []string{WantsTool, PlacementTool, FindingsTool, IndexerFindingsTool, "seat:mesh-hand-acts.hand-act"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-assigner.views.placement",
if !slices.Equal(m.Invokes, []string{WantsTool, AssignmentsTool, FindingsTool, IndexerFindingsTool, "seat:mesh-hand-acts.hand-act"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.findings", "mesh-delivery-indexer.views.findings"}) {
t.Errorf("invokes %v, consumes %v", m.Invokes, m.Consumes)
}
@@ -262,7 +262,7 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
buckets := []string{"mesh-controller_conditions", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_placement",
buckets := []string{"mesh-controller_conditions", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_assignments",
"mesh-healers_attempts"}
for _, o := range Observers {
buckets = append(buckets, o+"_findings")
+3 -3
View File
@@ -3,7 +3,7 @@ module mesh-conditions
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
@@ -30,6 +30,6 @@ require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
)
+2 -2
View File
@@ -39,14 +39,14 @@
],
"invokes": [
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-assigner.assigner_read_placement",
"mesh-delivery-assigner.assigner_read_assignments",
"mesh-delivery-assigner.assigner_read_findings",
"mesh-delivery-indexer.indexer_read_findings",
"seat:mesh-hand-acts.hand-act"
],
"consumes": [
"mesh-delivery-assigner.views.wants",
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.findings",
"mesh-delivery-indexer.views.findings"
],
+2 -2
View File
@@ -21,7 +21,7 @@ account alone writes its buckets.
## How a value resolves
For every module the seat mesh-module-assigner places on a node (`mesh-delivery-assigner.placement`), from the module's
For every module the seat mesh-module-assigner places on a node (`mesh-delivery-assigner.assignments`), from the module's
own manifests as the delivery built them, never from the controller (issue 1953): the trunk's, which the indexer's
`modules.<module>` names in its manifests by digest (`mesh-delivery-indexer.modules`, `.manifests`), with the build the
node's want names (`mesh-delivery-assigner.wants`, the builder's `mesh-delivery-builder.builds` naming its manifest)
@@ -30,7 +30,7 @@ module none of whose manifests can be read is listed under `unresolved` with why
values last resolved for it. Then: the manifest's default; mesh config for the predefined key the value falls back
to; node config for that key on the node; the module's own value on that node, the most specific winning. A value
naming no predefined key resolves from its default and the module's own value only (ADR 0348 §2). It resolves at
start, whenever a placement, a want or a trunk module moves, and every five minutes, writing only the values that moved.
start, whenever an assignment, a want or a trunk module moves, and every five minutes, writing only the values that moved.
## The verb
@@ -4,8 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -155,22 +155,21 @@ func (m *memStore) assign(t *testing.T, node, module string) {
m.place(t, node, module, true)
}
// unassign takes a module off a node's placement.
// unassign takes a module off a node's assignments.
func (m *memStore) unassign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, module, false)
}
// place writes a node's placement as the seat writes it: its node key and module keys (research 072 row 37), and for
// ace in the form before, `placement.ace`, which a reader still takes while the change is delivered.
// place writes a node's assignments as the seat writes it: its node key and module keys (research 072 row 37).
func (m *memStore) place(t *testing.T, node, module string, on bool) {
t.Helper()
keys := map[string][]byte{}
for _, k := range m.keysOf(PlacementRead) {
e, _ := m.Get(PlacementRead, k)
for _, k := range m.keysOf(AssignmentsRead) {
e, _ := m.Get(AssignmentsRead, k)
keys[k] = e.Value
}
all, err := assignerclient.PlacementsOf(keys)
all, err := assignerclient.AssignmentsOf(keys)
if err != nil {
t.Fatal(err)
}
@@ -186,25 +185,17 @@ func (m *memStore) place(t *testing.T, node, module string, on bool) {
kept = append(kept, meshseat.PlacedModule{Module: module})
}
p.Modules = kept
for _, k := range m.keysOf(PlacementRead) {
for _, k := range m.keysOf(AssignmentsRead) {
if n, _, _, err := assignerclient.ParseKey(k); err == nil && n == node {
_ = m.Delete(PlacementRead, k)
_ = m.Delete(AssignmentsRead, k)
}
}
if node == "ace" {
raw, err := assignerclient.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
_ = m.Put(PlacementRead, "placement.ace", json.RawMessage(raw))
return
}
values, err := assignerclient.PlacementValues(p)
values, err := assignerclient.AssignmentsValues(p)
if err != nil {
t.Fatal(err)
}
for k, raw := range values {
if err := m.Put(PlacementRead, k, json.RawMessage(raw)); err != nil {
if err := m.Put(AssignmentsRead, k, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
@@ -592,7 +583,7 @@ func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T
}
}
// **A placement, a declaration or its removal moves the resolved values**, written only where they moved, and
// **An assignment, a declaration or its removal moves the resolved values**, written only where they moved, and
// said as config-changed; a pass over unchanged sources writes nothing.
func TestAnAssignmentMovesTheResolvedValuesAndAQuietPassWritesNothing(t *testing.T) {
c, m := aMesh(t)
@@ -148,7 +148,7 @@ type Published struct {
Commit string `json:"commit"`
}
// Assignment is a module placed on a node, as the seat mesh-module-assigner's key `placement.<node>` names it.
// Assignment is a module placed on a node, as the seat mesh-module-assigner's key `<node>.node` names it.
type Assignment struct {
Node string `json:"node"`
Module string `json:"module"`
@@ -39,8 +39,8 @@ func TestEveryStateAndActHasItsTool(t *testing.T) {
via := map[string]string{
ValuesState: "config list", ResolvedState: "config list", PredefinedState: "config list",
ProposalsState: "config proposals", PullsState: "config list", KeptState: "config list", HistoryLog: "config history",
DeclaredState: DeclaredTool,
PlacementRead: "config list", WantsRead: "config list", BuildsRead: "config list", ModulesRead: DeclaredTool,
DeclaredState: DeclaredTool,
AssignmentsRead: "config list", WantsRead: "config list", BuildsRead: "config list", ModulesRead: DeclaredTool,
ManifestsRead: DeclaredTool, IndexerPredefinedRead: "config list", CandidatesRead: ResyncTool,
"a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool,
// design 32: each new state with the tool that reads it, each act with the tool that starts it.
@@ -2,7 +2,7 @@ package main
// What mesh-config reads of the delivery owners' state, through their tools and views families (novox/hq ADR 0615 §1,
// §4 to §6, ADR 0596): never a bucket of theirs. For each of the six states it follows (the indexer's modules, manifests,
// predefined and config-candidates, the assigner's placement and wants) it asks the owner's read tool for the whole
// predefined and config-candidates, the assigner's assignments and wants) it asks the owner's read tool for the whole
// state at its start, keeps the picture from the owner's `views.<state>` events, and asks again only on a gap. Started
// while an owner is down it keeps asking, and past two minutes writes a finding naming the owner and the tool.
//
@@ -27,12 +27,12 @@ import (
// The tools of the owners that mesh-config asks, by the state they read.
const (
IndexerModulesTool = "indexer_read_modules"
IndexerManifestsTool = "indexer_read_manifests"
IndexerPredefTool = "indexer_read_predefined"
IndexerCandidatesTool = "indexer_read_config_candidates"
AssignerPlacementTool = "assigner_read_placement"
AssignerWantsTool = "assigner_read_wants"
IndexerModulesTool = "indexer_read_modules"
IndexerManifestsTool = "indexer_read_manifests"
IndexerPredefTool = "indexer_read_predefined"
IndexerCandidatesTool = "indexer_read_config_candidates"
AssignerAssignmentsTool = "assigner_read_assignments"
AssignerWantsTool = "assigner_read_wants"
FirstViewsTool = "config_first_views"
FirstViewAgainTool = "config_first_view_again"
@@ -45,7 +45,7 @@ var followed = []struct{ Read, Owner, State, Tool string }{
{ManifestsRead, indexerModule, "manifests", IndexerManifestsTool},
{IndexerPredefinedRead, indexerModule, catalog.StatePredefined, IndexerPredefTool},
{CandidatesRead, indexerModule, catalog.StateConfigCandidates, IndexerCandidatesTool},
{PlacementRead, assignerModule, "placement", AssignerPlacementTool},
{AssignmentsRead, assignerModule, "assignments", AssignerAssignmentsTool},
{WantsRead, assignerModule, "wants", AssignerWantsTool},
}
@@ -208,7 +208,7 @@ func viewTools(c *Config, o func() *ownerViews) []stdio.Tool {
return []stdio.Tool{
{Name: FirstViewsTool,
Description: "mesh-config's first views of the delivery owners' state: for each of the indexer's modules, manifests, " +
"predefined and config-candidates and the assigner's placement and wants, the tool it asks, the revision its " +
"predefined and config-candidates and the assigner's assignments and wants, the tool it asks, the revision its " +
"picture holds, when it asked and was answered, how many asks (and by hand), and whether its condition is raised.",
Run: func(map[string]any) (any, error) {
ov := o()
@@ -3,7 +3,7 @@ package main
import (
"fmt"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
)
// The holders whose states this module reads, one constant each (a reader names its holder once; ADR 0634 §4).
@@ -3,7 +3,7 @@ package main
import (
"encoding/json"
"fmt"
meshseat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"sort"
@@ -23,10 +23,10 @@ const (
PredefinedState = "predefined"
ProposalsState = "proposals"
// PlacementRead is the seat mesh-module-assigner's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
// node's key `placement.<node>` with the modules placed there. It is the one source of which module runs where
// AssignmentsRead is the seat mesh-module-assigner's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
// node's key `<node>.node` with the modules placed there. It is the one source of which module runs where
// (issue 1953): the controller's assignments are no longer read.
PlacementRead = (assignerModule + "." + meshseat.StatePlacement)
AssignmentsRead = (assignerModule + "." + meshseat.StateAssignments)
// WantsRead is the assigner's wants, `want.<node>.<module>`: the build each node is to run (ADR 0539 §1).
WantsRead = assignerModule + ".wants"
// BuildsRead is the builder's builds, `<module>.<fingerprint hex>`: each build's manifest digest (ADR 0525).
+12 -13
View File
@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"sort"
@@ -20,8 +20,8 @@ func (c *Config) sources() (Sources, error) {
s := Sources{Declared: map[string]ModuleDeclarations{}, OnNode: map[string]ModuleDeclarations{},
Predefined: map[string]Published{}, Values: map[string]Value{}, Renames: map[string]Rename{}}
var problems []string
if err := c.readPlacement(&s); err != nil {
return s, fmt.Errorf("the seat mesh-module-assigner's placement cannot be read: %w", err)
if err := c.readAssignments(&s); err != nil {
return s, fmt.Errorf("the seat mesh-module-assigner's assignments cannot be read: %w", err)
}
if err := c.readDeclarations(&s); err != nil {
return s, fmt.Errorf("the modules' declarations cannot be read: %w", err)
@@ -64,28 +64,27 @@ func (c *Config) sources() (Sources, error) {
return s, nil
}
// readPlacement is every module the seat mesh-module-assigner places on a node (ADR 0567 §1, §9), from the state's keys in
// either form: a node key and its module keys, or the form before, `placement.<node>` (assignerclient.PlacementsOf;
// research 072 row 37). By node in name order, each node's modules in the order its placement names them.
func (c *Config) readPlacement(s *Sources) error {
// readAssignments is every module the seat mesh-module-assigner places on a node (ADR 0567 §1, §9), from the state's keys, a
// node key and its module keys (assignerclient.AssignmentsOf; research 072 row 37). By node in name order, each node's modules in the order its assignments names them.
func (c *Config) readAssignments(s *Sources) error {
keys := map[string][]byte{}
if err := c.each(PlacementRead, func(key string, raw []byte) error {
if err := c.each(AssignmentsRead, func(key string, raw []byte) error {
keys[key] = raw
return nil
}); err != nil {
return err
}
placements, err := assignerclient.PlacementsOf(keys)
assignments, err := assignerclient.AssignmentsOf(keys)
if err != nil {
return fmt.Errorf("the placement is not read whole: %w", err)
return fmt.Errorf("the assignments is not read whole: %w", err)
}
nodes := make([]string, 0, len(placements))
for node := range placements {
nodes := make([]string, 0, len(assignments))
for node := range assignments {
nodes = append(nodes, node)
}
sort.Strings(nodes)
for _, node := range nodes {
for _, module := range assignerclient.Assigned(placements[node]) {
for _, module := range assignerclient.Assigned(assignments[node]) {
s.Assignments = append(s.Assignments, Assignment{Node: node, Module: module})
}
}
@@ -18,10 +18,10 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -591,7 +591,7 @@ func (p *player) buildIn(node, module, fp, raw string) {
func (p *player) placed(node string, modules ...string) {
p.t.Helper()
pl := meshseat.Placement{V: 1, Node: node}
pl := meshseat.Assignments{V: 1, Node: node}
for _, m := range modules {
pl.Modules = append(pl.Modules, meshseat.PlacedModule{Module: m})
}
@@ -602,7 +602,7 @@ func (p *player) placed(node string, modules ...string) {
p.t.Fatal(err)
}
for _, w := range writes {
p.put("mesh-delivery-assigner.placement", w.Key, json.RawMessage(w.Value))
p.put("mesh-delivery-assigner.assignments", w.Key, json.RawMessage(w.Value))
}
}
@@ -658,7 +658,7 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
buckets := []string{"mesh-delivery-assigner_placement", "mesh-delivery-assigner_wants", "mesh-delivery-builder_builds",
buckets := []string{"mesh-delivery-assigner_assignments", "mesh-delivery-assigner_wants", "mesh-delivery-builder_builds",
"mesh-delivery-indexer_modules", "mesh-delivery-indexer_manifests", "mesh-delivery-indexer_predefined",
"mesh-delivery-indexer_config-candidates"}
for _, s := range []string{ValuesState, ResolvedState, PredefinedState, ProposalsState, PullsState, KeptState, DeclaredState,
+3 -3
View File
@@ -4,13 +4,13 @@ go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
+2 -2
View File
@@ -32,7 +32,7 @@
"mesh-delivery-indexer.views.manifests",
"mesh-delivery-indexer.views.predefined",
"mesh-delivery-indexer.views.config-candidates",
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.wants"
],
"invokes": [
@@ -47,7 +47,7 @@
"mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_config_candidates",
"mesh-delivery-assigner.assigner_read_placement",
"mesh-delivery-assigner.assigner_read_assignments",
"mesh-delivery-assigner.assigner_read_wants"
],
"reads": [
@@ -163,7 +163,7 @@ func (h *Holder) groupMergeable(gid, target string, reads []memberRead) bool {
}
// passWaits is why an approved merge is not handed yet because another approved merge's pass is under way into a
// repository it goes into; empty when none is (issue 1275, 2026-10-26 13:40: group feat/890-placement merged
// repository it goes into; empty when none is (issue 1275, 2026-10-26 13:40: group feat/890-assignments merged
// mesh-delivery#72 between the members of fix/734-lost-ask, whose mesh-delivery#73 then conflicted). A pass is under way
// from its first member handed until every member is merged; one half merged stays under way while it is refused or held,
// until it is whole, declined, or its remaining members are gone or asked anew (a new head is a new Approve). The first
@@ -12,7 +12,7 @@ import (
// Issue 1275, met a third time (2026-10-26 13:40, group fix/734-lost-ask): the operator approved mesh-sdk#92,
// mesh-controller#472 and mesh-delivery#73; the first two merged, the forge answered #73's merge 405 "Please try again
// later", and in the seconds between, group feat/890-placement merged mesh-delivery#72, so #73 then conflicted. A group is
// later", and in the seconds between, group feat/890-assignments merged mesh-delivery#72, so #73 then conflicted. A group is
// merged only when every member merges now, and no other approved merge lands in a repository between its members.
const (
@@ -134,7 +134,7 @@ func handedWords(ps []MergePull) string {
}
// Two approved merges into one repository: the one whose pass started has the repository until every member merged, and
// the other is handed only then (2026-10-26: mesh-delivery#72 of feat/890-placement merged between fix/734-lost-ask's
// the other is handed only then (2026-10-26: mesh-delivery#72 of feat/890-assignments merged between fix/734-lost-ask's
// members). This holds while mesh/check gates no merge too (ADR 0551): it waits for a merge, never for a check.
func TestTwoApprovedMergesIntoOneRepositoryDoNotInterleave(t *testing.T) {
for _, gates := range []bool{true, false} {
@@ -150,8 +150,8 @@ func TestTwoApprovedMergesIntoOneRepositoryDoNotInterleave(t *testing.T) {
if got := handedWords(handing(w)); got != "novox/mesh-sdk#92" {
t.Fatalf("handed %q, wanted the group's first member", got)
}
// feat/890-placement: a lone pull request into mesh-delivery, approved while the group's pass is under way.
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-placement", "modules/mesh-delivery-checker/go.mod"))
// feat/890-assignments: a lone pull request into mesh-delivery, approved while the group's pass is under way.
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-assignments", "modules/mesh-delivery-checker/go.mod"))
w.h.Checked(judged(wDel, 72, wH72, "pass", gTrunk0))
setChecks(w, wDel, wH72, "success", "success")
w.settleAll()
@@ -307,10 +307,10 @@ func TestAHeldHalfMergedGroupKeepsItsRepositoriesAndNoTwoPassesWaitOnEachOther(t
if len(w.forge.merges) != 2 {
t.Fatalf("the case: mesh-sdk#92 merged, mesh-delivery#73 refused: %v", w.forge.merges)
}
approveSecondGroup(t, w, "feat/890-placement", groupB...)
approveSecondGroup(t, w, "feat/890-assignments", groupB...)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("group feat/890-placement merged while feat/g is half merged into a repository both go into: %v", w.forge.merges)
t.Fatalf("group feat/890-assignments merged while feat/g is half merged into a repository both go into: %v", w.forge.merges)
}
w.forge.refuseRepo = nil
if _, err := w.h.MergeAgain(IDOf(wDel, wH73), "the forge said try again later"); err != nil {
@@ -334,13 +334,13 @@ func TestAHeldHalfMergedGroupKeepsItsRepositoriesAndNoTwoPassesWaitOnEachOther(t
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.merges.manual = true
approveSecondGroup(t, w, "feat/890-placement", groupB...)
approveSecondGroup(t, w, "feat/890-assignments", groupB...)
w.settleAll()
if got := handedWords(handing(w)); got != "novox/mesh-controller#472" {
t.Fatalf("the other group's pass did not start: handed %q", got)
}
_, err := w.h.MergeAgain(IDOf(wSDK, wH92), "asked again")
if err == nil || !strings.Contains(err.Error(), "feat/890-placement") {
if err == nil || !strings.Contains(err.Error(), "feat/890-assignments") {
t.Fatalf("asked again while a pass it waits for is under way: %v", err)
}
})
+2 -2
View File
@@ -31,8 +31,8 @@ require (
require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
+4 -4
View File
@@ -5,7 +5,7 @@ P5). Go, one binary (`cmd/mesh-provisioner-client`), run as a service of its own
`mesh-provisioner-client`, outside the tool runner, on a bus account of its own, `provisioner.<node>`, delivered as
its own secret `provisioner-broker` (mesh-sdk `buscompose/provisioner.go`). The bus grants that account its own
machine's keys and, to read alone, the seat mesh-provisioner's whole `plans`, where a provider's consumers on any
machine are bound to it (ADR 0373), and the seat mesh-module-assigner's whole `placement`; nothing else.
machine are bound to it (ADR 0373), and the seat mesh-module-assigner's whole `assignments`; nothing else.
## What it does
@@ -13,10 +13,10 @@ For each module placed on its node — a plan in the seat mesh-provisioner's `pl
`<node>.<module>`, novox/hq ADR 0570 clause 2); a module the seat mesh-module-assigner still places here stays while the
seat holds no plan of it, filled from its last plan, and leaves only when neither names it (issue 1126): the templates archive
by reference and digest, the files it fills (path, template, `owner-at-runtime`, how its program takes a change)
and the placement values its templates name — the client:
and the assignments values its templates name — the client:
- fetches the templates archive and uses it only when it hashes to the plan's digest, keeping it by digest;
- watches each owner's keys for its node: placement in the plan, config in `mesh-config_resolved`
- watches each owner's keys for its node: assignments in the plan, config in `mesh-config_resolved`
(`<node>.<module>.<key>`), both named by `mesh-provisioner_registry`;
- asks the vault (`mesh-vault.get`, named by the registry) for each of the module's own secrets its plan names
(`provisioning.secrets`), in its own name on the subject naming its account, with the public half of a key it
@@ -78,7 +78,7 @@ and the placement values its templates name — the client:
It fills `config`, `secret` (design 57 V4, issue 647), the collections (`collected`, and `shell`, `environment`
and `contribution` read as collections: design 57 L1, issue 684), `consumers` and `bound` (issue 679) and the
placement kinds (`dir`, `access`, `port`, `seat`, `machine`). Any other kind fails with the reason.
assignments kinds (`dir`, `access`, `port`, `seat`, `machine`). Any other kind fails with the reason.
## A module's code asks at runtime
@@ -1,10 +1,10 @@
package main
import (
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"bytes"
@@ -36,10 +36,10 @@ import (
// back. A module taken off the node loses its needs and its report; the node-engine removes the files the
// declaration no longer names.
//
// The owners it fills from are placement (in the module's plan), config (mesh-config_resolved) and the vault, asked for
// The owners it fills from are assignments (in the module's plan), config (mesh-config_resolved) and the vault, asked for
// each secret the module's plan names (secrets.go), and for each consumer's credential a provider here lists
// (consumers.go); and each provider's entries for a consumer here. Every key it writes is its own machine's; it reads
// the plans whole, for who binds a provider here (ADR 0373), and the seat mesh-module-assigner's placement, for which
// the plans whole, for who binds a provider here (ADR 0373), and the seat mesh-module-assigner's assignments, for which
// modules stay here: its bus account is granted no other.
// The holders whose states this module reads, each named once here (held equal to the manifest's `reads` by a unit row).
@@ -57,14 +57,14 @@ const (
// PlansBucket is where each module's provisioning plan on each node is read: the seat mesh-provisioner's
// `plans` (novox/hq ADR 0570 clause 2), read as an assignment's value carried it (asAssignment).
PlansBucket = provisionerModule + "_" + meshprovisionerclient.PlansState
// PlacementBucket is the seat mesh-module-assigner's placement (novox/hq ADR 0567): a module it names on this node stays
// AssignmentsBucket is the seat mesh-module-assigner's assignments (novox/hq ADR 0567): a module it names on this node stays
// here while the seat mesh-provisioner holds no plan of it (nats on novox runs without a want), filled from its
// last plan, and leaves only when neither names it (issue 1126 B12; the controller's assignments mirror, read for
// this before, is frozen since the controller's end, ADR 0590).
PlacementBucket = assignerModule + "_" + mesh.StatePlacement
ConfigBucket = "mesh-config_resolved"
RegistryBucket = "mesh-provisioner_registry"
AppliedBucket = "mesh-provisioner_applied"
AssignmentsBucket = assignerModule + "_" + mesh.StateAssignments
ConfigBucket = "mesh-config_resolved"
RegistryBucket = "mesh-provisioner_registry"
AppliedBucket = "mesh-provisioner_applied"
)
// AppliedSubject is where the client says what it applied for one module (ADR 0349 clauses 4 and 15).
@@ -167,15 +167,15 @@ type state struct {
plans map[string]Plan
// seatPlans is the seat's plan by key, as an assignment's value reads.
seatPlans map[string][]byte
// placement is the seat mesh-module-assigner's keys as read, and placed the modules it names on this node.
placement *assignerclient.Keys
placed map[string]bool
refused map[string]string // a module whose plan cannot be acted on, and why
config map[string]map[string]json.RawMessage
applied map[string]bool // module → a report is kept
reported map[string][]byte // module → the last report said, undated
last map[string]Report // module → the last report said, dated
secrets map[string]heldSecret // the vault's name → what this run holds of it; never written anywhere
// assignments is the seat mesh-module-assigner's keys as read, and placed the modules it names on this node.
assignments *assignerclient.Keys
placed map[string]bool
refused map[string]string // a module whose plan cannot be acted on, and why
config map[string]map[string]json.RawMessage
applied map[string]bool // module → a report is kept
reported map[string][]byte // module → the last report said, undated
last map[string]Report // module → the last report said, dated
secrets map[string]heldSecret // the vault's name → what this run holds of it; never written anywhere
// items is every item recorded for this node, by collection and contributor: what a collector is filled from.
items map[string]map[string]itemValue
// recorded is each item's key and its value as kept: what this client compares before it records again.
@@ -217,7 +217,7 @@ func (c *Client) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{PlansBucket, PlacementBucket, ConfigBucket, RegistryBucket, AppliedBucket,
for _, b := range []string{PlansBucket, AssignmentsBucket, ConfigBucket, RegistryBucket, AppliedBucket,
CollectBucket} {
kv, err := c.JS.KeyValue(ctx, b)
if err != nil {
@@ -255,9 +255,9 @@ func (c *Client) Run(ctx context.Context) error {
}
mine := c.Node + ".>"
// The registry whole, and every machine's plan: a provider here is bound by consumers on any machine, in their plans
// (ADR 0373); and placement whole, as its account is granted it (its node key and the legacy key are both read).
// (ADR 0373); and assignments whole, as its account is granted it (its node key and the legacy key are both read).
// Every other bucket, this machine's keys alone.
for _, b := range []string{RegistryBucket, PlansBucket, PlacementBucket} {
for _, b := range []string{RegistryBucket, PlansBucket, AssignmentsBucket} {
if err := watch(b, ">"); err != nil {
return err
}
@@ -347,13 +347,13 @@ func (c *Client) Run(ctx context.Context) error {
config: map[string]map[string]json.RawMessage{}, applied: map[string]bool{},
reported: map[string][]byte{}, secrets: map[string]heldSecret{}, items: map[string]map[string]itemValue{},
recorded: map[string][]byte{}, assigned: map[string]bool{}, binds: map[string]binding{},
placement: assignerclient.NewKeys(), placed: map[string]bool{},
assignments: assignerclient.NewKeys(), placed: map[string]bool{},
providing: map[string]bool{}, leftOut: map[string]string{}, bound: map[string]map[string][]byte{},
boundReady: map[string]bool{}, unplaced: map[string]bool{}, boundUsers: map[string]map[string]bool{}}
// Every current value first: nothing is filled from half of them.
current := map[string]bool{}
var early []*nats.Msg
for len(current) < 6 { // the registry, the plans, placement, config, applied, collect
for len(current) < 6 { // the registry, the plans, assignments, config, applied, collect
select {
case <-ctx.Done():
return ctx.Err()
@@ -519,7 +519,7 @@ func ownComposedOf(e jetstream.KeyValueEntry) string {
return h.Composed
}
// told says what an update moved of a module's config, or of its placement, which decides whether it is answered
// told says what an update moved of a module's config, or of its assignments, which decides whether it is answered
// at all (asks.go).
func (c *Client) told(u update) {
rest, ok := strings.CutPrefix(u.entry.Key(), c.Node+".")
@@ -579,8 +579,8 @@ func (c *Client) take(s *state, u update) []string {
}
// A kind's answer moves what every module waits on.
return sortedKeys(s.plans)
case PlacementBucket:
return c.takePlacement(s, key, e.Value(), e.Revision(), gone)
case AssignmentsBucket:
return c.takeAssignments(s, key, e.Value(), e.Revision(), gone)
case PlansBucket:
if s.seatPlans == nil {
s.seatPlans = map[string][]byte{}
@@ -599,8 +599,8 @@ func (c *Client) take(s *state, u update) []string {
// key, so the module leaves the machine as it does when the key goes (issue 1098).
gone = gone || !provider.Standing(value)
if module, mine := strings.CutPrefix(key, c.Node+"."); mine && gone && s.placed[module] {
// Placement still names it: it stays, filled from its last plan and binding as it did, until neither names
// it (takePlacement).
// Assignments still names it: it stays, filled from its last plan and binding as it did, until neither names
// it (takeAssignments).
return []string{module}
}
// Whom it binds, whichever machine's it is: a provider here listing its consumers is filled again.
@@ -1173,19 +1173,19 @@ func (c *Client) forgetItems(ctx context.Context, kvs map[string]jetstream.KeyVa
return removed
}
// takePlacement folds one key of the seat mesh-module-assigner's placement in, and answers the modules of this node it moved:
// takeAssignments folds one key of the seat mesh-module-assigner's assignments in, and answers the modules of this node it moved:
// one it names now stays here without a plan; one it no longer names, with no plan of the seat, leaves.
func (c *Client) takePlacement(s *state, key string, value []byte, revision uint64, gone bool) []string {
node, err := s.placement.Put(key, value, revision, gone)
func (c *Client) takeAssignments(s *state, key string, value []byte, revision uint64, gone bool) []string {
node, err := s.assignments.Put(key, value, revision, gone)
if node != c.Node {
return nil
}
if err != nil {
logf("[provisioner-client] the placement's %s cannot be read, so this node's placement stays as it was: %v", key, err)
logf("[provisioner-client] the assignments' %s cannot be read, so this node's assignments stays as it was: %v", key, err)
return nil
}
now := map[string]bool{}
if p, _, ok := s.placement.Of(c.Node); ok {
if p, _, ok := s.assignments.Of(c.Node); ok {
for _, m := range p.Modules {
now[m.Module] = true
}
@@ -8,10 +8,10 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"net"
"net/http"
"net/http/httptest"
@@ -84,7 +84,7 @@ func startBus(t *testing.T) *aBus {
t.Fatal(err)
}
b := &aBus{url: s.ClientURL(), domain: js, conn: nc, kv: map[string]jetstream.KeyValue{}}
for _, name := range []string{PlansBucket, PlacementBucket, ConfigBucket, RegistryBucket, AppliedBucket,
for _, name := range []string{PlansBucket, AssignmentsBucket, ConfigBucket, RegistryBucket, AppliedBucket,
CollectBucket, haveBucket} {
kv, err := js.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: name})
if err != nil {
@@ -125,18 +125,18 @@ func (b *aBus) put(t *testing.T, bucket, key, value string) {
}
}
// place writes this node's placement as the seat mesh-module-assigner keeps it, naming exactly these modules.
// place writes this node's assignments as the seat mesh-module-assigner keeps it, naming exactly these modules.
func (b *aBus) place(t *testing.T, modules ...string) {
t.Helper()
p := mesh.Placement{V: 1, Node: node}
p := mesh.Assignments{V: 1, Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, mesh.PlacedModule{Module: m})
}
keys := assignerclient.NewKeys()
ctx := context.Background()
have, _ := b.kv[PlacementBucket].Keys(ctx)
have, _ := b.kv[AssignmentsBucket].Keys(ctx)
for _, k := range have {
if e, err := b.kv[PlacementBucket].Get(ctx, k); err == nil {
if e, err := b.kv[AssignmentsBucket].Get(ctx, k); err == nil {
_, _ = keys.Put(k, e.Value(), e.Revision(), false)
}
}
@@ -146,10 +146,10 @@ func (b *aBus) place(t *testing.T, modules ...string) {
}
for _, w := range writes {
if w.Value == nil {
b.del(t, PlacementBucket, w.Key)
b.del(t, AssignmentsBucket, w.Key)
continue
}
if _, err := b.kv[PlacementBucket].Put(ctx, w.Key, w.Value); err != nil {
if _, err := b.kv[AssignmentsBucket].Put(ctx, w.Key, w.Value); err != nil {
t.Fatal(err)
}
}
@@ -448,7 +448,7 @@ func eventually(t *testing.T, what string, ok func() bool) {
const dunstrc = "/home/nox/.config/dunst/dunstrc"
// dunst is a module with one owned file naming a config value and a placement value.
// dunst is a module with one owned file naming a config value and an assignment value.
func dunst(t *testing.T) Plan {
tpl, _ := templates(t, map[string]string{
"dunstrc": "[global]\nfont = Monospace ${config:font-size}\nicons = ${machine:account-home}/.icons\n"})
@@ -662,10 +662,10 @@ func TestNatsAModuleTakenOffTheNodeLosesItsReport(t *testing.T) {
}
// Issue 1126 B12 (ADR 0590): the controller's assignments are frozen and go, so what places a module here is the seat
// mesh-module-assigner's placement alone. A module stays while placement names it, even with no plan of the seat
// mesh-module-assigner's assignments alone. A module stays while assignments names it, even with no plan of the seat
// mesh-provisioner (nats on novox runs without a want): its last plan keeps being filled, and it leaves only when
// neither its plan nor its placement is there.
func TestNatsAModuleStaysWhilePlacementNamesItAndLeavesOnlyWhenNeitherPlacesIt(t *testing.T) {
// neither its plan nor its assignments is there.
func TestNatsAModuleStaysWhileAssignmentsNamesItAndLeavesOnlyWhenNeitherPlacesIt(t *testing.T) {
b := startBus(t)
heard := b.listen(t)
e := startEngine(t, map[string]string{dunstrc: "owned"})
@@ -677,14 +677,14 @@ func TestNatsAModuleStaysWhilePlacementNamesItAndLeavesOnlyWhenNeitherPlacesIt(t
return strings.Contains(e.read(t, dunstrc), "Monospace 13")
})
// The seat's plan goes, placement stays: the module stays, filled from its last plan, its report kept.
// The seat's plan goes, assignments stays: the module stays, filled from its last plan, its report kept.
b.del(t, PlansBucket, node+".dunst")
b.config(t, "dunst", "font-size", "15")
eventually(t, "dunstrc follows a change with only placement naming it", func() bool {
eventually(t, "dunstrc follows a change with only assignments naming it", func() bool {
return strings.Contains(e.read(t, dunstrc), "Monospace 15")
})
if r, _ := heard.last("dunst"); r.Status == StatusRemoved || len(b.keys(t, AppliedBucket, node+".dunst")) != 1 {
t.Fatalf("the module left when its plan went, though placement still names it: %+v", r)
t.Fatalf("the module left when its plan went, though assignments still names it: %+v", r)
}
// Neither places it: it leaves.
@@ -756,10 +756,10 @@ func TestNatsWhatCannotBeFilledFailsWithTheReason(t *testing.T) {
}
}
// TestNatsResticsFilesFillFromTheBackupPlacement is the backup lines as placement (issue 794): restic's two files
// TestNatsResticsFilesFillFromTheBackupAssignments is the backup lines as assignments (issue 794): restic's two files
// are the plan's `backup:lines` and `backup:data`, each exactly the text the controller composed, trailing newline
// and all, and a changed plan refills them.
func TestNatsResticsFilesFillFromTheBackupPlacement(t *testing.T) {
func TestNatsResticsFilesFillFromTheBackupAssignments(t *testing.T) {
const backups, data = "/var/lib/restic/backups.conf", "/var/lib/restic/data.conf"
b := startBus(t)
e := startEngine(t, map[string]string{backups: "owned", data: "owned"})
@@ -15,7 +15,7 @@ import (
// their providers' entries carry them, the console's port from node-tools, the secrets and the per-holder read
// credential from the vault.
//
// The controller's placement (nothing refused or unplaced) and the byte-identity of every file not handed are checked
// The controller's assignments (nothing refused or unplaced) and the byte-identity of every file not handed are checked
// against the controller itself, not here: this module cannot import its internals (the report of issue 761).
func catalogFile(t *testing.T, path string) string {
@@ -146,7 +146,7 @@ type PlannedFile struct {
Takes string `json:"takes,omitempty"`
}
// The placeholder kinds placement answers, as the plan's Placement keys begin (ADR 0349 clause 2).
// The placeholder kinds assignments answers, as the plan's Assignments keys begin (ADR 0349 clause 2).
var placementKinds = []string{"dir", "access", "port", "seat", "machine", "backup"}
var (
@@ -345,7 +345,7 @@ func (p Plan) problems() []string {
for k := range p.Placement {
kind, name, ok := strings.Cut(k, ":")
if !ok || name == "" || !slices.Contains(placementKinds, kind) {
out = append(out, fmt.Sprintf("placement %q is not <kind>:<name> of a placement kind", k))
out = append(out, fmt.Sprintf("assignments %q is not <kind>:<name> of an assignment kind", k))
}
}
slices.Sort(out)
@@ -11,7 +11,7 @@ accounts {
users = [
{ user: "domain", password: "x" }
{ user: "provisioner.g14", password: "x", permissions: {
publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.g14.node", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.g14.node", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_node-engine_have.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.g14.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_plans.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.DELETE.KV_postgres_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-adapter_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-proxy_bound.>", "$JS.API.CONSUMER.DELETE.KV_node-tools_bound.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.g14.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.g14.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-provisioner_plans", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_registry", "$JS.API.STREAM.INFO.KV_postgres_bound", "$JS.API.STREAM.INFO.KV_route-adapter_bound", "$JS.API.STREAM.INFO.KV_route-proxy_bound", "$JS.API.STREAM.INFO.KV_node-tools_bound", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-provisioner_plans.>", "$JS.FC.KV_mesh-delivery-assigner_placement.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_registry.>", "$JS.FC.KV_postgres_bound.>", "$JS.FC.KV_route-adapter_bound.>", "$JS.FC.KV_route-proxy_bound.>", "$JS.FC.KV_node-tools_bound.>", "$KV.mesh-provisioner_applied.g14.>", "$KV.mesh-provisioner_collect.g14.>", "mesh.seat.mesh-vault.call.get.provisioner~g14", "prov.applied.g14.>", "prov.changed.g14.>"] }
publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.g14.node", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.g14.node", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_node-engine_have.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.g14.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_plans.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.DELETE.KV_postgres_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-adapter_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-proxy_bound.>", "$JS.API.CONSUMER.DELETE.KV_node-tools_bound.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_assignments.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.g14.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.g14.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-provisioner_plans", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_registry", "$JS.API.STREAM.INFO.KV_postgres_bound", "$JS.API.STREAM.INFO.KV_route-adapter_bound", "$JS.API.STREAM.INFO.KV_route-proxy_bound", "$JS.API.STREAM.INFO.KV_node-tools_bound", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-provisioner_plans.>", "$JS.FC.KV_mesh-delivery-assigner_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_registry.>", "$JS.FC.KV_postgres_bound.>", "$JS.FC.KV_route-adapter_bound.>", "$JS.FC.KV_route-proxy_bound.>", "$JS.FC.KV_node-tools_bound.>", "$KV.mesh-provisioner_applied.g14.>", "$KV.mesh-provisioner_collect.g14.>", "mesh.seat.mesh-vault.call.get.provisioner~g14", "prov.applied.g14.>", "prov.changed.g14.>"] }
subscribe: { allow: ["_INBOX.provisioner.g14.>", "mesh.mod.mesh-vault.event.secret-changed", "prov.ask.g14"] }
allow_responses: { max: 1, ttl: "1m" }
} }
+2 -2
View File
@@ -5,8 +5,8 @@ go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
github.com/nats-io/nats-server/v2 v2.11.17
+2 -2
View File
@@ -11,9 +11,9 @@ is the module `mesh-provisioner-client`.
`secret`, `bound` — the provider's own bucket, `<provider-module>_bound`, keyed
`<node>.<module>.<provision>[.<local>]` (mesh-sdk `go/provider`) — `consumers` — a provider's consumers'
credentials, asked of the vault by the provider machine's client (novox/hq ADR 0373) — `collected` — and `shell`, `environment` and `contribution`, read as collections of the same
name (design 57 L1) — and the placement namespaces `dir`, `access`, `port`, `seat`, `machine`). Each
name (design 57 L1) — and the assignments namespaces `dir`, `access`, `port`, `seat`, `machine`). Each
answer names the owning module and where a client finds the values: a domain's bucket and its key shape
(placement's is the controller's assignments mirror, whose value carries the provisioner's plan of a module handed
(the assignments' is the controller's assignments mirror, whose value carries the provisioner's plan of a module handed
to it), or a seat's verb asked on request and reply (a secret is never in a bucket). Compiled in
`registry.go`, written at start and every hour, only where it differs; a kind no domain answers is deleted.
- `mesh-provisioner_needs`: what each module on each node needs, `<node>.<module>.<kind>.<name>`.
@@ -12,7 +12,9 @@ package main
import (
"context"
"encoding/json"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"os"
@@ -63,7 +65,23 @@ func published(t *testing.T, s *memStore, c today) {
s.write(t, ComposedRead, mustKey(engineclient.HaveNodeKey(c.Node)), `{"v":1,"node":"`+c.Node+`","resources":[]}`)
}
// goldenMesh is the store as the provisioner reads it for frombustest's mesh: the indexer's modules, n1's placement, the
// writeAssignments writes a node's assignments as the seat does, its node key and a key per module (research 072 row 37).
func writeAssignments(t *testing.T, s *memStore, node string, modules ...string) {
t.Helper()
a := meshseat.Assignments{V: assignerclient.Version, Node: node}
for _, m := range modules {
a.Modules = append(a.Modules, meshseat.PlacedModule{Module: m})
}
values, err := assignerclient.AssignmentsValues(a)
if err != nil {
t.Fatal(err)
}
for key, raw := range values {
s.write(t, AssignmentsRead, key, string(raw))
}
}
// goldenMesh is the store as the provisioner reads it for frombustest's mesh: the indexer's modules, n1's assignments, the
// vault's on n2, restic's want.
func goldenMesh(t *testing.T, c today) *memStore {
t.Helper()
@@ -80,9 +98,8 @@ func goldenMesh(t *testing.T, c today) *memStore {
s.write(t, ManifestsRead, mustKey(catalogclient.ManifestsKey(digest)), string(value))
s.write(t, ModulesRead, module, `{"v":1,"fp2":"`+fp+`","manifest":"`+digest+`"}`)
}
s.write(t, PlacementRead, "placement."+c.Node, `{"v":1,"node":"`+c.Node+`","decided":{},"modules":[{"module":"store"},`+
`{"module":"postgres"},{"module":"mesh-provisioner-client"},{"module":"restic"}]}`)
s.write(t, PlacementRead, "placement.n2", `{"v":1,"node":"n2","decided":{},"modules":[{"module":"mesh-vault"}]}`)
writeAssignments(t, s, c.Node, "store", "postgres", "mesh-provisioner-client", "restic")
writeAssignments(t, s, "n2", "mesh-vault")
s.write(t, WantsRead, "want."+c.Node+".restic", `{"v":1,"fp":"`+c.KeptAt["restic"]+`","self":true}`)
return s
}
@@ -6,7 +6,7 @@ package main
// (`<node>.<consumer>.<provision>[.<local>]`), which it alone writes. It composes nothing: each plan is the one the
// node-engine's published composition carries (`composed-<node>`, ADR 0593 §6), the node-engine alone composing.
//
// - **Made per node.** A change of a node's placement, of one of its wants or of one of its published compositions
// - **Made per node.** A change of a node's assignments, of one of its wants or of one of its published compositions
// makes that node's plans again; a change of the indexer's module does it for the nodes the module is placed on.
// Each node's plans are read twice: at the builds its wants name (its plans, what its client applies) and at the
// newest build of each placed module, so a build that adds a requirement has its key before its want (clause 2).
@@ -31,9 +31,9 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -69,11 +69,11 @@ type Refusal struct {
// The reads it makes the plans from. ComposedRead is every node's published compositions, `composed-<node>` (ADR 0593
// §6), read as the node-engine's named state: its keys are `<node>.<module>.<fingerprint hex>` and `<node>.node`.
const (
PlacementRead = (assignerModule + "." + meshseat.StatePlacement)
WantsRead = assignerModule + ".wants"
ModulesRead = indexerModule + "." + catalog.StateModules
ManifestsRead = indexerModule + "." + catalog.StateManifests
HaveRead = "node-engine.have"
AssignmentsRead = (assignerModule + "." + meshseat.StateAssignments)
WantsRead = assignerModule + ".wants"
ModulesRead = indexerModule + "." + catalog.StateModules
ManifestsRead = indexerModule + "." + catalog.StateManifests
HaveRead = "node-engine.have"
// BuildsRead is the builder's builds, `<module>.<fingerprint part>`: the key of the build a provider runs names the
// digest of the manifest it was built from (novox/hq ADR 0603 clause 5).
BuildsRead = builds.BuilderModule + ".builds"
@@ -609,7 +609,7 @@ func (h *Holder) judgeNode(node string, needed map[string]meshprovisionerclient.
case manyAtOnce(len(leaving), len(consumers)) && !retireMany:
fault := fmt.Sprintf("On %s, one pass would retire the provisions of %d of the %d consumers that hold any (%s): "+
"most likely a read stopped listing them (a node's compositions published in part, a want, a have or the "+
"placement not read whole), not their removal. Nothing is retired and nothing is asked; the keys stay as they "+
"assignments not read whole), not their removal. Nothing is retired and nothing is asked; the keys stay as they "+
"were. If the removal is meant, provisioner_compose with retire-many retires them, each asked.",
node, len(leaving), len(consumers), strings.Join(sortedKeys(leaving), ", "))
hold(fault)
@@ -1321,8 +1321,8 @@ func (h *Holder) put(key string, p meshprovisionerclient.Provision) {
// ---- what a change touches --------------------------------------------------------------------------------------
// NodesOfPlacement is the node a placement key names, in either form (research 072 row 37).
func NodesOfPlacement(key string) []string {
// NodesOfAssignments is the node an assignment key names (research 072 row 37).
func NodesOfAssignments(key string) []string {
if n, _, _, err := assignerclient.ParseKey(key); err == nil {
return []string{n}
}
@@ -1356,20 +1356,20 @@ func (h *Holder) NodesOfModule(module string) []string {
return sortedKeys(nodes)
}
// placedOn is every node a module is placed on, sorted, from the placement's keys in either form
// (assignerclient.PlacementsOf; research 072 row 37). A node whose keys do not read whole is read as they stand.
// placedOn is every node a module is placed on, sorted, from the assignments' keys
// (assignerclient.AssignmentsOf; research 072 row 37). A node whose keys do not read whole is read as they stand.
func (h *Holder) placedOn(module string) []string {
nodes := map[string]bool{}
raw := map[string][]byte{}
if keys, err := h.Store.Keys(PlacementRead); err == nil {
if keys, err := h.Store.Keys(AssignmentsRead); err == nil {
for _, k := range keys {
if e, err := h.Store.Get(PlacementRead, k); err == nil && e != nil {
if e, err := h.Store.Get(AssignmentsRead, k); err == nil && e != nil {
raw[k] = e.Value
}
}
}
placements, _ := assignerclient.PlacementsOf(raw)
for n, p := range placements {
assignments, _ := assignerclient.AssignmentsOf(raw)
for n, p := range assignments {
for _, m := range p.Modules {
if m.Module == module {
nodes[n] = true
@@ -1578,22 +1578,22 @@ func publishedOn(s Store, node string) (map[string]publishedPlan, error) {
// placedModules is every module placed on node, sorted, and the indexer's newest fingerprint (FP2) of each.
func placedModules(s Store, node string) ([]string, map[string]string, error) {
raw := map[string][]byte{}
keys, err := s.Keys(PlacementRead)
keys, err := s.Keys(AssignmentsRead)
if err != nil {
return nil, nil, fmt.Errorf("the placement cannot be read: %w", err)
return nil, nil, fmt.Errorf("the assignments cannot be read: %w", err)
}
for _, k := range keys {
if n, _, _, err := assignerclient.ParseKey(k); err != nil || n != node {
continue
}
if e, err := s.Get(PlacementRead, k); err == nil && e != nil {
if e, err := s.Get(AssignmentsRead, k); err == nil && e != nil {
raw[k] = e.Value
}
}
placements, _ := assignerclient.PlacementsOf(raw)
assignments, _ := assignerclient.AssignmentsOf(raw)
var placed []string
fp2 := map[string]string{}
for _, m := range placements[node].Modules {
for _, m := range assignments[node].Modules {
placed = append(placed, m.Module)
mk, err := catalogclient.ModulesKey(m.Module)
if err != nil {
@@ -1609,10 +1609,10 @@ func placedModules(s Store, node string) ([]string, map[string]string, error) {
return placed, fp2, nil
}
// WatchedReads are the states the holder reads whole, each from the watched copy (ADR 0580 clause 2): the placement,
// WatchedReads are the states the holder reads whole, each from the watched copy (ADR 0580 clause 2): the assignments,
// the wants, the indexer's modules, the haves and the published compositions, and the providers' entries, whose watch
// is also what rejudges a key.
var WatchedReads = append([]string{PlacementRead, WantsRead, ModulesRead, HaveRead, ComposedRead}, boundReads()...)
var WatchedReads = append([]string{AssignmentsRead, WantsRead, ModulesRead, HaveRead, ComposedRead}, boundReads()...)
// OwnStates are the states the holder alone writes (ADR 0570 clause 1), read from the watched copy too (issue 1954: some
// 250 to 400 reads by key per composition): each of its own writes is taken into the copy at once (WatchedStates.Wrote),
@@ -6,7 +6,7 @@ package main
// states and every input it reads are a real nats-server's key-value buckets, read through the watched copy of those
// buckets (ADR 0580 clause 2); the plans come from n1's published compositions in `composed-n1` (ADR 0593 §6), as
// captured from the composition of mesh-sdk's frombustest mesh (restic on n1, requiring postgres-database from postgres
// there), and nothing is composed. A placement, a want and a published composition make the key; the providers'
// there), and nothing is composed. An assignment, a want and a published composition make the key; the providers'
// outcomes (the vault's entry for the credential, postgres's own have) turn it provided; restic leaving the node makes
// it retiring and raises the ask.
@@ -14,6 +14,8 @@ import (
"context"
"encoding/json"
"errors"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -128,7 +130,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
var c today
readJSON(t, "testdata/today-composed.json", &c)
node := c.Node
// The bus as the holder reads it: the indexer's modules and manifests, n1's placement and the vault's on n2, the
// The bus as the holder reads it: the indexer's modules and manifests, n1's assignments and the vault's on n2, the
// want of restic on n1, and what n1's engine published.
mem := goldenMesh(t, c)
published(t, mem, c)
@@ -210,7 +212,7 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
if p := state(key); p.State != meshprovisionerclient.Asked || p.Provider.Module != "postgres" {
t.Fatalf("the placement and the want composed %+v", p)
t.Fatalf("the assignments and the want composed %+v", p)
}
if e, _ := s.Get(PlansState, node+".restic"); e == nil {
t.Fatal("restic's plan is not on the bus")
@@ -263,14 +265,24 @@ func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatalf("the outcomes did not turn it provided: %+v", p)
}
// restic leaves the node and its want goes: retiring, one ask. Its compositions stay until its engine deletes them.
if err := s.Put(PlacementRead, "placement."+node, json.RawMessage(`{"v":1,"node":"`+node+`","decided":{},`+
`"modules":[{"module":"store"},{"module":"postgres"},{"module":"mesh-provisioner-client"}]}`)); err != nil {
left := meshseat.Assignments{V: assignerclient.Version, Node: node, Modules: []meshseat.PlacedModule{{Module: "store"}, {Module: "postgres"},
{Module: "mesh-provisioner-client"}}}
values, err := assignerclient.AssignmentsValues(left)
if err != nil {
t.Fatal(err)
}
for k, raw := range values {
if err := s.Put(AssignmentsRead, k, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
if err := s.Delete(AssignmentsRead, node+".restic"); err != nil {
t.Fatal(err)
}
if err := s.Delete(WantsRead, "want."+node+".restic"); err != nil {
t.Fatal(err)
}
settled(PlacementRead, "placement."+node)
settled(AssignmentsRead, node+".node")
settled(WantsRead, "want."+node+".restic")
if _, err := h.ComposeNode(ctx, node); err != nil {
t.Fatal(err)
@@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
meshseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -225,15 +225,15 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
// A provision's credential is the vault's own provision, on the consumer's node for a login of its own, and the
// provision depends on it (clauses 3, 6).
c := newMesh()
// The placement in its keys per module and node (research 072 row 37).
novox, err := assignerclient.PlacementValues(meshseat.Placement{Node: "novox", Modules: []meshseat.PlacedModule{{Module: "mesh-vault"}}})
// The assignments in its keys per module and node (research 072 row 37).
novox, err := assignerclient.AssignmentsValues(meshseat.Assignments{Node: "novox", Modules: []meshseat.PlacedModule{{Module: "mesh-vault"}}})
if err != nil {
t.Fatal(err)
}
for k, v := range novox {
c.s.write(t, PlacementRead, k, string(v))
if n := NodesOfPlacement(k); !slices.Equal(n, []string{"novox"}) {
t.Errorf("NodesOfPlacement(%q) = %v", k, n)
c.s.write(t, AssignmentsRead, k, string(v))
if n := NodesOfAssignments(k); !slices.Equal(n, []string{"novox"}) {
t.Errorf("NodesOfAssignments(%q) = %v", k, n)
}
}
pair := pgDB
@@ -749,8 +749,7 @@ func TestARetirementIsJudgedOnlyFromAWholePicture(t *testing.T) {
readJSON(t, "testdata/today-composed.json", &c)
s := goldenMesh(t, c)
published(t, s, c)
s.write(t, PlacementRead, "placement."+c.Node, `{"v":1,"node":"`+c.Node+`","decided":{},"modules":[{"module":"store"},`+
`{"module":"postgres"},{"module":"mesh-provisioner-client"},{"module":"restic"},{"module":"de-spiegel"}]}`)
writeAssignments(t, s, c.Node, "store", "postgres", "mesh-provisioner-client", "restic", "de-spiegel")
got := FromCompositions(s)(context.Background(), c.Node, map[string]string{"photos": fp})
if !strings.Contains(got.AtWants.Held["de-spiegel"], "no build") || !slices.Equal(got.Unpublished, []string{"photos"}) {
t.Errorf("held %v, unpublished %v", got.AtWants.Held, got.Unpublished)
@@ -102,18 +102,18 @@ func (l *loads) missing() string {
return strings.Join(out, ", ")
}
// heldBack is, once every read has loaded, every node the placement names: the passes their loads held back.
// heldBack is, once every read has loaded, every node the assignments names: the passes their loads held back.
func (l *loads) heldBack(h *Holder) []string {
if l.missing() != "" {
return nil
}
keys, err := h.Store.Keys(PlacementRead)
keys, err := h.Store.Keys(AssignmentsRead)
if err != nil {
return nil
}
nodes := map[string]bool{}
for _, k := range keys {
for _, n := range NodesOfPlacement(k) {
for _, n := range NodesOfAssignments(k) {
nodes[n] = true
}
}
@@ -176,17 +176,17 @@ func slowest(took []Took) time.Duration {
// The owners' read tools of the states the provisioner follows, which the manifest invokes (ADR 0615).
const (
PlacementTool = assignerModule + ".assigner_read_placement"
WantsTool = assignerModule + ".assigner_read_wants"
ModulesTool = indexerModule + ".indexer_read_modules"
ManifestsTool = indexerModule + ".indexer_read_manifests"
AssignmentsTool = assignerModule + ".assigner_read_assignments"
WantsTool = assignerModule + ".assigner_read_wants"
ModulesTool = indexerModule + ".indexer_read_modules"
ManifestsTool = indexerModule + ".indexer_read_manifests"
)
// newViews is the provisioner's pictures of the assigner's placement and wants and the indexer's modules and manifests:
// newViews is the provisioner's pictures of the assigner's assignments and wants and the indexer's modules and manifests:
// each asked once of its owner's read tool, then followed by the owner's events (ADR 0615).
func newViews() *views.Reader {
v := &views.Reader{Module: Module, Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
v.Follow(assignerModule, "placement", PlacementTool)
v.Follow(assignerModule, "assignments", AssignmentsTool)
v.Follow(assignerModule, "wants", WantsTool)
v.Follow(indexerModule, "modules", ModulesTool)
v.Follow(indexerModule, "manifests", ManifestsTool)
@@ -236,7 +236,7 @@ func main() {
w := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)}
// No pass judges before every read it composes from has loaded once (issue 940); each load wakes every node it
// held back, through its follower's handler.
loads := newLoads(PlacementRead, WantsRead, ComposedRead, HaveRead, ModulesRead)
loads := newLoads(AssignmentsRead, WantsRead, ComposedRead, HaveRead, ModulesRead)
h.Loaded = loads.missing
go w.run(h)
go starting(h)
@@ -248,7 +248,7 @@ func main() {
w.add(loads.heldBack(h)...)
}()
}
follow(PlacementRead, func(c stdio.StateChange) error { w.add(NodesOfPlacement(c.Key)...); return nil })
follow(AssignmentsRead, func(c stdio.StateChange) error { w.add(NodesOfAssignments(c.Key)...); return nil })
follow(WantsRead, func(c stdio.StateChange) error { w.add(NodesOfWant(c.Key)...); return nil })
follow(ComposedRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
follow(HaveRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
@@ -74,7 +74,7 @@ func (m *memStore) write(t *testing.T, state, key, raw string) {
}
// The registry answers every kind a moved module's template may name (ADR 0349 clause 2): config, secret, bound,
// collected and the placement namespaces, each with its domain and where a client finds it.
// collected and the assignments namespaces, each with its domain and where a client finds it.
func TestTheRegistryAnswersEveryKindATemplateMayName(t *testing.T) {
var kinds []string
for _, a := range TheRegistry {
@@ -104,7 +104,7 @@ func TestTheRegistryAnswersEveryKindATemplateMayName(t *testing.T) {
t.Errorf("the registry's kinds are %v, want %v", kinds, want)
}
// The buckets a client is granted to read its node's keys of (mesh-controller internal/broker/provisioner.go):
// the config domain's resolved values, this module's own collections, and placement in each module's assignment,
// the config domain's resolved values, this module's own collections, and assignments in each module's assignment,
// where the controller carries the provisioner's plan (issue 671): a client never reads the node's declaration.
for kind, bucket := range map[string]string{"config": "mesh-config_resolved", "collected": "mesh-provisioner_" + Collect,
"shell": "mesh-provisioner_" + Collect, "environment": "mesh-provisioner_" + Collect,
@@ -340,9 +340,9 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
}
// The builds read serves only haveOutcome's offer check (ADR 0603 clause 5: which manifest the build a provider runs
// was made from) and makes no plan, so ADR 0593 §8 ("it composes nothing") holds.
// The assigner's placement and wants and the indexer's modules and manifests are asked once of their owners' read
// The assigner's assignments and wants and the indexer's modules and manifests are asked once of their owners' read
// tools and followed by their events, not read (ADR 0615 §1).
followed := []string{PlacementRead, WantsRead, ModulesRead, ManifestsRead}
followed := []string{AssignmentsRead, WantsRead, ModulesRead, ManifestsRead}
for _, r := range append(slices.Clone(WatchedReads), BuildsRead) {
if slices.Contains(followed, r) {
if slices.Contains(m.Reads, r) {
@@ -354,8 +354,8 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
t.Errorf("the manifest does not read %s, which the holder reads", r)
}
}
if !slices.Equal(m.Invokes, []string{PlacementTool, WantsTool, ModulesTool, ManifestsTool, "seat:mesh-hand-acts.hand-act"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.wants",
if !slices.Equal(m.Invokes, []string{AssignmentsTool, WantsTool, ModulesTool, ManifestsTool, "seat:mesh-hand-acts.hand-act"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests"}) {
t.Errorf("invokes %v, consumes %v", m.Invokes, m.Consumes)
}
@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
)
// The holders whose states this module reads, one constant each (a reader names its holder once; ADR 0634 §4).
@@ -48,15 +48,15 @@ type Answer struct {
Decision string `json:"decision"`
}
// placement is the seat's: where a module runs, its directories, accesses, ports, seats and the machine's facts (ADR
// assignments is the seat's: where a module runs, its directories, accesses, ports, seats and the machine's facts (ADR
// 0349 clause 6), in the provisioning plan this seat composes of each module on each node from the bus, its state
// `plans` (novox/hq ADR 0570 clauses 1, 2): a client never reads the node's declaration, which the node-engine keeps for
// root alone, nor the controller's assignments.
func placement(kind, what string) Answer {
func assignments(kind, what string) Answer {
return Answer{Kind: kind, Domain: Module, From: FromBucket, Bucket: Module + "_" + PlansState,
Key: "<node>.<module>",
Why: what + ": placement, in the provisioning plan the seat mesh-provisioner composes of the module on the node " +
"(plans, plan.placement)", Decision: "novox/hq ADR 0570 clause 1"}
Why: what + ": assignments, in the provisioning plan the seat mesh-provisioner composes of the module on the node " +
"(plans, plan.assignments)", Decision: "novox/hq ADR 0570 clause 1"}
}
// oldCollection is a namespace read as a collection of its own name (novox/hq ADR 0349 clause 13, design 57 L1).
@@ -95,12 +95,12 @@ var TheRegistry = []Answer{
oldCollection("shell", "a shell's code in one slot, or a session file's or a power moment's"),
oldCollection("environment", "the account's environment in one format"),
oldCollection("contribution", "what a seat's holder receives of one kind"),
placement("dir", "a module's directory"),
placement("access", "a directory a module reaches of another's"),
placement("port", "a module's port"),
placement("seat", "where a seat's holder is reached"),
placement("machine", "a fact of the machine"),
placement("backup", "what the machine's modules back up: `lines` and `data`, as the controller derives them from each module's declared data"),
assignments("dir", "a module's directory"),
assignments("access", "a directory a module reaches of another's"),
assignments("port", "a module's port"),
assignments("seat", "where a seat's holder is reached"),
assignments("machine", "a fact of the machine"),
assignments("backup", "what the machine's modules back up: `lines` and `data`, as the controller derives them from each module's declared data"),
}
// Publish brings the registry bucket to TheRegistry: a kind whose answer differs is written, a kind no longer in
+3 -3
View File
@@ -3,13 +3,13 @@ module mesh-provisioner
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
+2 -2
View File
@@ -30,14 +30,14 @@
"mesh-vault.bound"
],
"invokes": [
"mesh-delivery-assigner.assigner_read_placement",
"mesh-delivery-assigner.assigner_read_assignments",
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_manifests",
"seat:mesh-hand-acts.hand-act"
],
"consumes": [
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.manifests"
@@ -12,7 +12,7 @@ import (
"os"
"time"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -77,7 +77,7 @@ func NewChecker() *Checker {
// self-check's own (first-view-unanswered), written into its state findings.
func newViews() *views.Reader {
r := &views.Reader{Module: Module, Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
r.Follow(assignerModule, placing.StatePlacement, PlacementTool)
r.Follow(assignerModule, placing.StateAssignments, AssignmentsTool)
r.Follow(assignerModule, placing.StateSeatHolders, SeatHoldersTool)
r.Follow(indexerModule, "modules", ModulesTool)
r.Follow(indexerModule, "manifests", ManifestsTool)
@@ -7,7 +7,7 @@ import (
"slices"
"testing"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
)
type manifest struct {
@@ -71,13 +71,13 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
if !slices.Equal(m.Consume, Consumes) {
t.Errorf("consumes %v, want the views families the probes follow, %v", m.Consume, Consumes)
}
for _, view := range []string{assignerModule + ".views." + placing.StatePlacement, assignerModule + ".views." + placing.StateSeatHolders} {
for _, view := range []string{assignerModule + ".views." + placing.StateAssignments, assignerModule + ".views." + placing.StateSeatHolders} {
if !slices.Contains(m.Consume, view) {
t.Errorf("the probes follow %s, the manifest consumes %v", view, m.Consume)
}
}
if PlacementRead != "mesh-delivery-assigner.placement" || SeatHoldersRead != "mesh-delivery-assigner.seat-holders" {
t.Errorf("the states are read as %s and %s", PlacementRead, SeatHoldersRead)
if AssignmentsRead != "mesh-delivery-assigner.assignments" || SeatHoldersRead != "mesh-delivery-assigner.seat-holders" {
t.Errorf("the states are read as %s and %s", AssignmentsRead, SeatHoldersRead)
}
states := map[string]int{}
for _, s := range m.State {
@@ -34,11 +34,11 @@ const (
// Invokes are the tools and seat verbs the probes ask, and Reads the states they read: the manifest's `invokes` and
// `reads`, which its test holds to these.
var (
Invokes = []string{StreamsTool, ClosedTool, SignalsVerb, BannedVerb, PlacementTool, SeatHoldersTool, ModulesTool, ManifestsTool, HandActVerb}
Invokes = []string{StreamsTool, ClosedTool, SignalsVerb, BannedVerb, AssignmentsTool, SeatHoldersTool, ModulesTool, ManifestsTool, HandActVerb}
Reads = []string{PresenceRead, HealthRead, ReadingsRead, GoodRead}
// Consumes are the views families the probes follow (ADR 0615): the indexer's and the assigner's states are asked
// once, by their owners' read tools, and then followed by their events, never read.
Consumes = []string{"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.seat-holders",
Consumes = []string{"mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.seat-holders",
"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests"}
)
@@ -47,7 +47,7 @@ const HandActVerb = "seat:mesh-hand-acts.hand-act"
// The owners' read tools of the states the probes follow, which the manifest invokes.
const (
PlacementTool = "mesh-delivery-assigner.assigner_read_placement"
AssignmentsTool = "mesh-delivery-assigner.assigner_read_assignments"
SeatHoldersTool = "mesh-delivery-assigner.assigner_read_seat_holders"
ModulesTool = "mesh-delivery-indexer.indexer_read_modules"
ManifestsTool = "mesh-delivery-indexer.indexer_read_manifests"
@@ -1,7 +1,7 @@
package main
// The probes of the mesh's machines (novox/hq ADR 0564 §6, issue 1952): each reads its facts where they live on the
// bus — placement (the seat mesh-module-assigner's state), who holds which seat (its seat-holders), whether a node is heard
// bus — assignments (the seat mesh-module-assigner's state), who holds which seat (its seat-holders), whether a node is heard
// from (the watchman's presence) — and asks each node's seat holder what only that node knows. Its assertion, kind,
// key and words are the controller's (mesh-controller probes.go), so the condition the keeper raises from it is the one
// the controller raised.
@@ -17,16 +17,16 @@ import (
"strings"
"sync"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
watchdogsclient "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
)
// The states the machine probes read (declared under `reads`), and the verb D8 asks each node.
const (
PlacementRead = assignerModule + "." + placing.StatePlacement
AssignmentsRead = assignerModule + "." + placing.StateAssignments
SeatHoldersRead = assignerModule + "." + placing.StateSeatHolders
PresenceRead = watchmanModule + ".presence"
BannedSeat = "node-intrusion-prevention"
@@ -34,26 +34,26 @@ const (
ScopeMachine = meshconditionsclient.ScopeMachine
)
// placements is every node's placement, by node, from the state's keys in either form: a node key and its module keys,
// or the form before, `placement.<node>` (assignerclient.PlacementsOf; research 072 row 37).
func (c *Checker) placements() (map[string]placing.Placement, error) {
keys, err := c.Store.Keys(PlacementRead)
// assignments is every node's assignments, by node, from the state's keys, a node key and its module keys
// (assignerclient.AssignmentsOf; research 072 row 37).
func (c *Checker) assignments() (map[string]placing.Assignments, error) {
keys, err := c.Store.Keys(AssignmentsRead)
if err != nil {
return nil, fmt.Errorf("the placement cannot be read: %w", err)
return nil, fmt.Errorf("the assignments cannot be read: %w", err)
}
values := map[string][]byte{}
for _, k := range keys {
raw, ok, err := c.Store.Get(PlacementRead, k)
raw, ok, err := c.Store.Get(AssignmentsRead, k)
if err != nil {
return nil, fmt.Errorf("the placement's key %s cannot be read: %w", k, err)
return nil, fmt.Errorf("the assignments' key %s cannot be read: %w", k, err)
}
if ok {
values[k] = raw
}
}
out, err := assignerclient.PlacementsOf(values)
out, err := assignerclient.AssignmentsOf(values)
if err != nil {
return nil, fmt.Errorf("the placement is not readable: %w", err)
return nil, fmt.Errorf("the assignments is not readable: %w", err)
}
return out, nil
}
@@ -126,10 +126,10 @@ var lookupHost = func(ctx context.Context, host string) ([]string, error) {
}
// probeBans is D8: no address the mesh owns is in any machine's ban list. The mesh's own addresses are every machine's
// private address and the address its endpoint names, from placement; the ban lists are asked of each machine's holder
// private address and the address its endpoint names, from assignments; the ban lists are asked of each machine's holder
// of node-intrusion-prevention that is heard from.
func probeBans(ctx context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
places, err := c.placements()
places, err := c.assignments()
if err != nil {
return nil, err
}
@@ -15,9 +15,9 @@ import (
"time"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
)
// The engine's words and bounds the judgement reads (mesh-host rootsearch).
@@ -188,8 +188,8 @@ type confinement struct {
Why string
}
// confined judges node's agent account from its placement, its report and its presence.
func (c *Checker) confined(node string, place placing.Placement, heard bool) (confinement, error) {
// confined judges node's agent account from its assignments, its report and its presence.
func (c *Checker) confined(node string, place placing.Assignments, heard bool) (confinement, error) {
agent := place.Decided.AgentAccount
if agent == "" {
return confinement{Why: fmt.Sprintf("%s names no agent account: agents run as the operator account (%s)",
@@ -206,7 +206,7 @@ func (c *Checker) confined(node string, place placing.Placement, heard bool) (co
// probeAgentAccounts is DA: every machine that names an agent account has it judged, on its node-engine's newest
// statement, unable to become root without a person (ADR 0266).
func probeAgentAccounts(_ context.Context, c *Checker) ([]meshconditionsclient.Observation, error) {
places, err := c.placements()
places, err := c.assignments()
if err != nil {
return nil, err
}
@@ -9,12 +9,12 @@ import (
"testing"
"time"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
watchdogsclient "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
@@ -140,7 +140,7 @@ func TestTheBusProbes(t *testing.T) {
{"H-bus a stream missing", probeBusHealth,
bus{drop: map[string]bool{"NODES": true}}, []string{"bus.mesh.unhealthy"}, "", nil},
{"H-bus no round trip", probeBusHealth, bus{asksFail: errors.New("timeout")}, nil, "no request crossed", nil},
// D8 (issue 238): placement's addresses and endpoints against each heard holder's ban list.
// D8 (issue 238): the assignments' addresses and endpoints against each heard holder's ban list.
{"D8 nothing of the mesh banned", probeBans, bus{banned: map[string]string{"ace": `{"banned":["198.51.100.7"]}`,
"novox": `{"banned":[]}`}}, nil, "", machines()},
{"D8 a private address and an endpoint banned", probeBans, bus{banned: map[string]string{
@@ -265,7 +265,7 @@ func machines() *memStore {
node, address, endpoint string
online bool
}{{"novox", "10.10.0.1/16", "203.0.113.5:51820", true}, {"ace", "10.10.0.2/16", "", true}, {"g14", "10.10.0.3/16", "", false}} {
pl := placing.Placement{V: 1, Node: p.node,
pl := placing.Assignments{V: 1, Node: p.node,
Decided: placing.NodeDecided{Address: p.address, Endpoint: p.endpoint, Hub: p.endpoint != ""}}
if p.node == "ace" {
pl.Modules = []placing.PlacedModule{{Module: "sshd"}} // a module key beside the node key
@@ -279,28 +279,22 @@ func machines() *memStore {
return s
}
// place writes a node's placement as the seat does, its node key and a key per module (research 072 row 37); g14's in
// the form before, `placement.g14`, which the probes still read while the change is delivered.
func place(s *memStore, p placing.Placement) {
if p.Node == "g14" {
raw, _ := assignerclient.EncodePlacement(p)
_ = s.Put(PlacementRead, "placement.g14", json.RawMessage(raw))
return
}
values, _ := assignerclient.PlacementValues(p)
// place writes a node's assignments as the seat does, its node key and a key per module (research 072 row 37).
func place(s *memStore, p placing.Assignments) {
values, _ := assignerclient.AssignmentsValues(p)
for k, raw := range values {
_ = s.Put(PlacementRead, k, json.RawMessage(raw))
_ = s.Put(AssignmentsRead, k, json.RawMessage(raw))
}
}
// placed is every node's placement the store holds.
func placed(s *memStore) map[string]placing.Placement {
keys, _ := s.Keys(PlacementRead)
// placed is every node's assignments the store holds.
func placed(s *memStore) map[string]placing.Assignments {
keys, _ := s.Keys(AssignmentsRead)
values := map[string][]byte{}
for _, k := range keys {
values[k], _, _ = s.Get(PlacementRead, k)
values[k], _, _ = s.Get(AssignmentsRead, k)
}
out, _ := assignerclient.PlacementsOf(values)
out, _ := assignerclient.AssignmentsOf(values)
return out
}
+2 -2
View File
@@ -8,8 +8,8 @@ require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
+2 -2
View File
@@ -21,7 +21,7 @@
"nats.nats_closed_connections",
"seat:mesh-watchdogs.signals",
"seat:node-intrusion-prevention.banned",
"mesh-delivery-assigner.assigner_read_placement",
"mesh-delivery-assigner.assigner_read_assignments",
"mesh-delivery-assigner.assigner_read_seat_holders",
"mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_manifests",
@@ -34,7 +34,7 @@
"restic.good"
],
"consumes": [
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.seat-holders",
"mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.manifests"
+7 -7
View File
@@ -58,8 +58,8 @@ type adoptionKept struct {
}
// heardHave is a node's latest have of the bus module, as the watch handed it, and whether it was adopted: a have heard
// while the placement does not yet name its node as holding the bus adopts nothing then, and the have may not change
// again for long, so it is adopted when the placement next changes (PlacementChanged), never on a timer.
// while the assignments does not yet name its node as holding the bus adopts nothing then, and the have may not change
// again for long, so it is adopted when the assignments next changes (AssignmentsChanged), never on a timer.
type heardHave struct {
raw []byte
by string
@@ -88,9 +88,9 @@ func (v *Vault) HaveGone(node string) {
v.mu.Unlock()
}
// PlacementChanged adopts from the kept have of the node the placement now names as holding the bus, when that have was
// not adopted yet: the placement's change is the event that makes it adoptable.
func (v *Vault) PlacementChanged() {
// AssignmentsChanged adopts from the kept have of the node the assignments now names as holding the bus, when that have was
// not adopted yet: the assignments' change is the event that makes it adoptable.
func (v *Vault) AssignmentsChanged() {
v.mu.Lock()
pending := 0
for _, h := range v.heard {
@@ -112,9 +112,9 @@ func (v *Vault) PlacementChanged() {
if !ok || h.adopted {
return
}
by := h.by + ", taken again when the placement named " + bus + " as holding the bus"
by := h.by + ", taken again when the assignments named " + bus + " as holding the bus"
if _, err := v.AdoptFrom(bus, h.raw, by, false); err != nil {
v.log("adopted nothing from %s.%s when the placement named it as holding the bus: %v", bus, BusModule, err)
v.log("adopted nothing from %s.%s when the assignments named it as holding the bus: %v", bus, BusModule, err)
return
}
v.markAdopted(bus, h.raw)
+1 -1
View File
@@ -81,7 +81,7 @@ const ProvisionerPrefix = "provisioner."
const AssignmentsRead = provisionerModule + "." + meshprovisionerclient.PlansState
// ProvisionsRead is the seat's state provisions as the manifest's `reads` names it; provisionerModule the module holding the seat
// mesh-provisioner (TestThePlacementNamesAreTheManifests).
// mesh-provisioner (TestTheAssignmentsNamesAreTheManifests).
const (
ProvisionsRead = provisionerModule + "." + meshprovisionerclient.ProvisionsState
provisionerModule = "mesh-provisioner"
@@ -1,7 +1,7 @@
package main
// The vault's reading of the seat mesh-module-assigner's placement, through its owner (novox/hq ADR 0615 §1, §4 to §6, ADR
// 0596): the assigner's tool `assigner_read_placement` asked for the whole state at start, the owner's `views.placement`
// The vault's reading of the seat mesh-module-assigner's assignments, through its owner (novox/hq ADR 0615 §1, §4 to §6, ADR
// 0596): the assigner's tool `assigner_read_assignments` asked for the whole state at start, the owner's `views.assignments`
// events after it, and the tool once more only on a gap. Started while the assigner is down it keeps asking and says so;
// it never reads the assigner's bucket instead. (The vault's other reads, the node-engines' haves, the grants part's
// accounts and the provisioner's plans and provisions, are not the indexer's or the assigner's state and stay as they are.)
@@ -19,34 +19,34 @@ import (
"git.novox.be/novox/mesh-sdk/go/views"
)
// PlacementTool is the assigner's ordinary read tool of its placement; PlacementViewsEvent the event of its views family.
// AssignmentsTool is the assigner's ordinary read tool of its assignments; AssignmentsViewsEvent the event of its views family.
const (
// assignerModule is the holder of the seat mesh-module-assigner, named from this module's manifest (invokes its read tool,
// consumes its views; TestThePlacementNamesAreTheManifests).
assignerModule = "mesh-delivery-assigner"
PlacementTool = assignerModule + ".assigner_read_placement"
PlacementViewsEvent = assignerModule + ".views.placement"
FirstViewsTool = "vault_first_views"
FirstViewAgainTool = "vault_first_view_again"
// consumes its views; TestTheAssignmentsNamesAreTheManifests).
assignerModule = "mesh-delivery-assigner"
AssignmentsTool = assignerModule + ".assigner_read_assignments"
AssignmentsViewsEvent = assignerModule + ".views.assignments"
FirstViewsTool = "vault_first_views"
FirstViewAgainTool = "vault_first_view_again"
)
// FindingsState is the vault's state of findings, which the condition keeper reads (ADR 0564): a finding names no secret.
const FindingsState = "findings"
// placementView is the picture; nil until main begins it.
var placementView *views.Follower
// assignmentsView is the picture; nil until main begins it.
var assignmentsView *views.Follower
// newPlacementView makes the follower: ask calls the tool, gone is told of a deleted key, changed of every change after
// newAssignmentsView makes the follower: ask calls the tool, gone is told of a deleted key, changed of every change after
// it is held (a first view's keys too).
func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string), changed func(),
func newAssignmentsView(ask func(key string, body any) (json.RawMessage, error), gone func(key string), changed func(),
findings views.FindingWriter) *views.Follower {
return &views.Follower{Owner: assignerModule, Tool: PlacementTool, State: "placement", Logf: say,
return &views.Follower{Owner: assignerModule, Tool: AssignmentsTool, State: "assignments", Logf: say,
Ask: func(_ context.Context, after string) (views.Answer, error) {
body := map[string]any{"values": true}
if after != "" {
body["after"] = after
}
raw, err := ask(PlacementTool, body)
raw, err := ask(AssignmentsTool, body)
if err != nil {
return views.Answer{}, err
}
@@ -55,7 +55,7 @@ func newPlacementView(ask func(key string, body any) (json.RawMessage, error), g
// Past its bound the vault's finding names the owner and the tool, and the condition keeper raises it (the
// answer deletes it); the vault keeps asking meanwhile.
Raise: func(s views.Status) {
say("the vault has no first view of the assigner's placement: %s has not answered since %s (%s); it keeps asking", s.Tool,
say("the vault has no first view of the assigner's assignments: %s has not answered since %s (%s); it keeps asking", s.Tool,
s.Asked.Format(time.RFC3339), s.Error)
f := views.UnansweredFinding(Seat, s, time.Now())
if err := findings.Put(f); err != nil {
@@ -88,24 +88,24 @@ func unwrapToolAnswer(raw json.RawMessage) []byte {
return raw
}
// beginPlacementView asks the first view until answered and hears the owner's events.
func beginPlacementView(f *views.Follower) {
// beginAssignmentsView asks the first view until answered and hears the owner's events.
func beginAssignmentsView(f *views.Follower) {
go func() {
if err := f.Start(context.Background()); err != nil {
say("the first view of the assigner's placement ended unanswered: %v", err)
say("the first view of the assigner's assignments ended unanswered: %v", err)
}
}()
go func() {
<-stdio.Served()
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
err := stdio.Subscribe(PlacementViewsEvent, func(e stdio.Envelope) error {
err := stdio.Subscribe(AssignmentsViewsEvent, func(e stdio.Envelope) error {
ev, err := views.DecodeEvent(e.Body)
if err != nil {
say("%s is not a views event: %v", PlacementViewsEvent, err)
say("%s is not a views event: %v", AssignmentsViewsEvent, err)
return nil
}
if gap, why := f.Apply(ev); gap {
say("a gap in the assigner's placement (%s): its first view is asked once more", why)
say("a gap in the assigner's assignments (%s): its first view is asked once more", why)
go func() { _ = f.Start(context.Background()) }()
}
return nil
@@ -113,29 +113,29 @@ func beginPlacementView(f *views.Follower) {
if err == nil {
return
}
say("does not hear %s yet (%v); asking again in %s", PlacementViewsEvent, err, wait)
say("does not hear %s yet (%v); asking again in %s", AssignmentsViewsEvent, err, wait)
time.Sleep(wait)
}
}()
}
// viewPlacement is the placement reader over the picture: a key asked before the first view is answered is an error, so
// viewAssignments is the assignments reader over the picture: a key asked before the first view is answered is an error, so
// the caller waits (the "not yet" of the vault's other reads), never a read of the owner's bucket.
func viewPlacement(f func() *views.Follower) PlacementReader {
func viewAssignments(f func() *views.Follower) AssignerReader {
ready := func() (*views.Follower, error) {
fol := f()
if fol == nil {
return nil, fmt.Errorf("the vault reads the assigner's placement only when it runs as the module")
return nil, fmt.Errorf("the vault reads the assigner's assignments only when it runs as the module")
}
select {
case <-fol.Ready():
return fol, nil
default:
s := fol.Status()
return nil, fmt.Errorf("the first view of the assigner's placement is not answered yet (%s asked %d times)", s.Tool, s.Asks)
return nil, fmt.Errorf("the first view of the assigner's assignments is not answered yet (%s asked %d times)", s.Tool, s.Asks)
}
}
return PlacementReader{
return AssignerReader{
Keys: func() ([]string, error) {
fol, err := ready()
if err != nil {
@@ -162,7 +162,7 @@ func viewPlacement(f func() *views.Follower) PlacementReader {
}
}
// leaverKey is whether a deleted key of the placement is a node's own key, `<node>.node`.
// leaverKey is whether a deleted key of the assignments is a node's own key, `<node>.node`.
func leaverKey(key string) (string, bool) {
node, ok := strings.CutSuffix(key, ".node")
return node, ok && node != ""
@@ -172,20 +172,20 @@ func leaverKey(key string) (string, bool) {
func viewTools(env *Env) []toolDef {
return []toolDef{
{Name: FirstViewsTool,
Description: "The vault's first view of the assigner's placement: the tool it asks, the revision its picture holds, when it " +
Description: "The vault's first view of the assigner's assignments: the tool it asks, the revision its picture holds, when it " +
"asked and was answered, how many asks (and by hand), and whether it is waiting.",
run: func(string, map[string]any) (any, error) {
if placementView == nil {
return nil, fmt.Errorf("the vault reads the assigner's placement only when it runs as the module")
if assignmentsView == nil {
return nil, fmt.Errorf("the vault reads the assigner's assignments only when it runs as the module")
}
return map[string]any{"first-views": []views.Status{placementView.Status()}}, nil
return map[string]any{"first-views": []views.Status{assignmentsView.Status()}}, nil
}},
{Name: FirstViewAgainTool,
Description: "Ask the first view of the assigner's placement once more by hand, with why, recorded in the vault's log.",
Description: "Ask the first view of the assigner's assignments once more by hand, with why, recorded in the vault's log.",
Input: map[string]any{"why": str("why the first view is asked again")},
run: func(caller string, a map[string]any) (any, error) {
if placementView == nil {
return nil, fmt.Errorf("the vault reads the assigner's placement only when it runs as the module")
if assignmentsView == nil {
return nil, fmt.Errorf("the vault reads the assigner's assignments only when it runs as the module")
}
why := strings.TrimSpace(arg(a, "why"))
if why == "" {
@@ -194,8 +194,8 @@ func viewTools(env *Env) []toolDef {
if err := fromAGrantedCaller(caller, FirstViewAgainTool); err != nil {
return nil, err
}
say("the first view of the assigner's placement is asked again by %s: %s", caller, why)
return map[string]any{"asked-again": true}, placementView.Again(context.Background(), why)
say("the first view of the assigner's assignments is asked again by %s: %s", caller, why)
return map[string]any{"asked-again": true}, assignmentsView.Again(context.Background(), why)
}},
}
}
@@ -13,8 +13,8 @@ import (
// ADR 0615 §6: the vault started while the assigner is down keeps asking, never reads its bucket, and past the bound
// writes a finding naming the owner and the tool for the condition keeper; the answer deletes it. Before the answer a read
// of the placement is an error.
func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing.T) {
// of the assignments is an error.
func TestAnUnansweredFirstViewOfTheAssignmentsIsAFindingTheAnswerClears(t *testing.T) {
var mu sync.Mutex
up := false
held := map[string]json.RawMessage{}
@@ -24,15 +24,15 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
if !up {
return nil, errors.New("nats: no responders available for request")
}
raw, _ := json.Marshal(views.Answer{V: 1, Owner: "mesh-delivery-assigner", State: "placement", Revision: 1, Created: time.Now(),
raw, _ := json.Marshal(views.Answer{V: 1, Owner: "mesh-delivery-assigner", State: "assignments", Revision: 1, Created: time.Now(),
Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}})
return raw, nil
}
f := newPlacementView(ask, func(string) {}, nil, heldFindings{&mu, held})
f := newAssignmentsView(ask, func(string) {}, nil, heldFindings{&mu, held})
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
reader := viewPlacement(func() *views.Follower { return f })
reader := viewAssignments(func() *views.Follower { return f })
if _, err := reader.Keys(); err == nil {
t.Fatal("the placement was read before its first view")
t.Fatal("the assignments was read before its first view")
}
done := make(chan error, 1)
go func() { done <- f.Start(context.Background()) }()
@@ -9,10 +9,10 @@ import (
"encoding/json"
"encoding/pem"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
enrollerclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"slices"
"sort"
"strings"
@@ -90,7 +90,7 @@ func newEnrolling(t *testing.T) *enrolling {
v := newVault(t, nil)
e := &enrolling{testVault: v, accounts: &fakeAccounts{keys: map[string]AccountHash{}}, logins: &loginsState{keys: map[string]NodeLogin{}}, acts: map[string]HandActSeen{}}
v.Accounts, v.Logins = e.accounts, e.logins
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "g14": {"web"}, "ace": {"shop"}})
v.Assigner = assignedNodes(t, map[string][]string{"novox": {"nats"}, "g14": {"web"}, "ace": {"shop"}})
v.LoadedListHeard(570)
v.TokenBase = func() (TokenBase, error) {
return TokenBase{Broker: "tls://10.77.0.1:4222", Fingerprint: "sha256:bus"}, nil
@@ -130,10 +130,10 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) {
}
// new1 is placed with its private-network address, as mesh-enrolment writes it before it asks for the certificate.
values := map[string][]byte{}
for node, p := range map[string]placing.Placement{"novox": {Node: "novox", Modules: []placing.PlacedModule{{Module: "nats"}}},
for node, p := range map[string]placing.Assignments{"novox": {Node: "novox", Modules: []placing.PlacedModule{{Module: "nats"}}},
"g14": {Node: "g14"}, "new1": {Node: "new1", Decided: placing.NodeDecided{Address: "10.77.0.9"}}} {
p.V = vaultclient.Version
kv, err := assignerclient.PlacementValues(p)
kv, err := assignerclient.AssignmentsValues(p)
if err != nil {
t.Fatal(err)
}
@@ -142,7 +142,7 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) {
}
_ = node
}
e.Placement = PlacementReader{Read: func(key string) ([]byte, error) { return values[key], nil },
e.Assigner = AssignerReader{Read: func(key string) ([]byte, error) { return values[key], nil },
Keys: func() ([]string, error) {
var out []string
for k := range values {
@@ -481,9 +481,9 @@ func said(p *published, event string) int {
return n
}
// 774/S48/AC2 (novox/hq ADR 0583 §1 insight): the key of a node that leaves the placement is deleted, heard from the
// placement's watch or found by the minute pass; one that cannot be read deletes nothing.
func TestANodesLoginsKeyGoesWhenItLeavesThePlacement(t *testing.T) {
// 774/S48/AC2 (novox/hq ADR 0583 §1 insight): the key of a node that leaves the assignments is deleted, heard from the
// the assignments' watch or found by the minute pass; one that cannot be read deletes nothing.
func TestANodesLoginsKeyGoesWhenItLeavesTheAssignments(t *testing.T) {
e := newEnrolling(t)
for _, n := range []string{"g14", "gone", "gone2"} {
e.logins.keys[n] = NodeLogin{Node: n, Login: "node." + n}
@@ -498,9 +498,9 @@ func TestANodesLoginsKeyGoesWhenItLeavesThePlacement(t *testing.T) {
t.Errorf("the keys after a node left and a pass: %+v", e.logins.keys)
}
e.logins.keys["gone"] = NodeLogin{Node: "gone"}
e.Placement = PlacementReader{}
e.Assigner = AssignerReader{}
e.LoginsPass()
if _, ok := e.logins.keys["gone"]; !ok {
t.Error("a placement that is not read deleted a key")
t.Error("an assignment that is not read deleted a key")
}
}
+9 -9
View File
@@ -61,7 +61,7 @@ func build() *Env {
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant,
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{},
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Assigner: seatAssignments, Signers: busSigners{},
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")),
RetireCallers: retireCallers(os.Getenv("MESH_VAULT_RETIRE_CALLERS")), Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")),
Bound: Bound{
@@ -110,21 +110,21 @@ func readHandAct(id string) (*HandActSeen, error) {
return nil, nil
}
// watchLeavers begins the picture of the placement and deletes a node's key of node-logins when its node key of the
// placement is deleted (noderotation.go): the owner's event, never a poll. It answers whether it began; the minute pass
// watchLeavers begins the picture of the assignments and deletes a node's key of node-logins when its node key of the
// assignments is deleted (noderotation.go): the owner's event, never a poll. It answers whether it began; the minute pass
// (LoginsPass) catches what it missed.
func watchLeavers(env *Env) bool {
if env.Vault == nil {
return true
}
if placementView == nil {
placementView = newPlacementView(stdio.Ask, func(key string) {
if assignmentsView == nil {
assignmentsView = newAssignmentsView(stdio.Ask, func(key string) {
if node, ok := leaverKey(key); ok {
env.Vault.NodeLeft(node)
}
}, env.Vault.PlacementChanged,
}, env.Vault.AssignmentsChanged,
meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
beginPlacementView(placementView)
beginAssignmentsView(assignmentsView)
}
return true
}
@@ -235,9 +235,9 @@ func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
return true
}
// placement reads the seat mesh-module-assigner's placement through its owner's tool and events (placementview.go), never the
// seatAssignments reads the seat mesh-module-assigner's assignments through its owner's tool and events (assignmentsview.go), never the
// owner's bucket.
var placement = viewPlacement(func() *views.Follower { return placementView })
var seatAssignments = viewAssignments(func() *views.Follower { return assignmentsView })
// readWant is one wanted account of the grants part, read afresh (issue-account).
func readWant(user string) ([]byte, bool, error) {
+1 -1
View File
@@ -406,7 +406,7 @@ func (v *Vault) IssueAccount(caller, user string) (any, error) {
return nil, fmt.Errorf("the wanted account %s could not be read: %w", user, err)
}
if !found {
return nil, fmt.Errorf("the grants part holds no wanted account %s: the vault mints only what placement composes "+
return nil, fmt.Errorf("the grants part holds no wanted account %s: the vault mints only what assignments composes "+
"(grants_accounts lists them)", user)
}
w, err := grantsstate.ReadWantedAccount(user, raw)
@@ -65,18 +65,18 @@ type player struct {
// ask answers a mesh/ask (a call of another module's tool); nil answers none, as a call with no responder.
ask func(key string, body json.RawMessage) (any, error)
// The assigner's placement as its owner's tool and views family say it (ADR 0615): the values, and the revision of the
// The assigner's assignments as its owner's tool and views family say it (ADR 0615): the values, and the revision of the
// last change, kept from the bucket.
placeMu sync.Mutex
placeValues map[string]json.RawMessage
placeRevision uint64
// answeredOnce closes answered at the vault's first view of the placement.
// answeredOnce closes answered at the vault's first view of the assignments.
answeredOnce sync.Once
answered chan struct{}
// The families the vault subscribed to, and the events said on one before it did: held, as the module's consumer on
// the bus holds them, and handed in order once it subscribes. A runtime that refused the subscription and dropped the
// events left the vault's picture at its first view, so a placement written after that view never reached it, and
// events left the vault's picture at its first view, so an assignment written after that view never reached it, and
// whether it did turned on which came first (the rotation test's flake, met twice).
subMu sync.Mutex
subscribed map[string]bool
@@ -132,11 +132,11 @@ func (p *player) subscribe(pattern string) {
var placeCreated = time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
// followPlacement keeps the bucket of the placement as the assigner's owner does: listed once, then each change said on
// followAssignments keeps the bucket of the assignments as the assigner's owner does: listed once, then each change said on
// its views family to the vault. Call it before the player serves.
func (p *player) followPlacement() {
func (p *player) followAssignments() {
p.t.Helper()
kv, err := p.js.KeyValue(p.ctx, "mesh-delivery-assigner_placement")
kv, err := p.js.KeyValue(p.ctx, "mesh-delivery-assigner_assignments")
if err != nil {
p.t.Fatal(err)
}
@@ -164,7 +164,7 @@ func (p *player) followPlacement() {
}
if !current {
p.placeRevision++
ev := map[string]any{"v": 1, "owner": "mesh-delivery-assigner", "state": "placement", "key": e.Key(),
ev := map[string]any{"v": 1, "owner": "mesh-delivery-assigner", "state": "assignments", "key": e.Key(),
"revision": p.placeRevision, "created": placeCreated}
if deleted {
ev["deleted"] = true
@@ -172,7 +172,7 @@ func (p *player) followPlacement() {
ev["value"] = json.RawMessage(e.Value())
}
body, _ := json.Marshal(ev)
p.event(PlacementViewsEvent, body)
p.event(AssignmentsViewsEvent, body)
}
p.placeMu.Unlock()
}
@@ -180,18 +180,18 @@ func (p *player) followPlacement() {
select {
case <-listed:
case <-time.After(10 * time.Second):
p.t.Fatal("the placement was not listed")
p.t.Fatal("the assignments was not listed")
}
}
// firstViewAnswered waits until the vault's first view of the placement is answered, so what the test writes after it
// firstViewAnswered waits until the vault's first view of the assignments is answered, so what the test writes after it
// reaches the vault only as the owner's events.
func (p *player) firstViewAnswered() {
p.t.Helper()
select {
case <-p.answered:
case <-time.After(30 * time.Second):
p.t.Fatal("the vault never asked its first view of the placement")
p.t.Fatal("the vault never asked its first view of the assignments")
}
}
@@ -364,9 +364,9 @@ func (p *player) handle(m wire) {
p.placeMu.Lock()
following := p.placeValues != nil
p.placeMu.Unlock()
if ask.Key == PlacementTool && following {
if ask.Key == AssignmentsTool && following {
p.placeMu.Lock()
answer := map[string]any{"v": 1, "owner": "mesh-delivery-assigner", "state": "placement", "revision": p.placeRevision,
answer := map[string]any{"v": 1, "owner": "mesh-delivery-assigner", "state": "assignments", "revision": p.placeRevision,
"created": placeCreated, "values": p.placeValues, "at": time.Now()}
raw, _ := json.Marshal(answer)
p.placeMu.Unlock()
@@ -492,7 +492,7 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
t.Fatal(err)
}
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts",
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_assignments"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
t.Fatal(err)
}
@@ -549,7 +549,7 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
{ID: "act-ok", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst token ret-1"}},
{ID: "act-other", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst other ret-1"}}}}, nil
}
p.followPlacement()
p.followAssignments()
go p.serve(stdout)
status := func() MintState {
@@ -16,7 +16,7 @@ package main
// `node-account-confirm` is the node-engine's, dialled as the new login, which the runtime proves: the old login's hash
// is then removed, and `rotation-done` is said once the loaded list lacks it, when the old value is dropped and the
// rotating part of the key cleared. `abandon` drops the new login and its value before the confirmation and keeps the
// old hash. A key whose node leaves the placement is deleted.
// old hash. A key whose node leaves the assignments is deleted.
import (
"encoding/json"
@@ -633,13 +633,13 @@ func (v *Vault) RotationState() RotationState {
return RotationState{Rotating: append([]NodeLogin{}, running...), Queued: append([]queuedRotation{}, v.nr.queue...)}
}
// LoginsPass deletes the key of every node that left the placement (ADR 0583 §1 insight). A placement that cannot be
// LoginsPass deletes the key of every node that left the assignments (ADR 0583 §1 insight). An assignment that cannot be
// read, or holds no node, deletes nothing.
func (v *Vault) LoginsPass() {
if v.Logins == nil || v.Placement.Keys == nil {
if v.Logins == nil || v.Assigner.Keys == nil {
return
}
placed, err := v.placements()
placed, err := v.assignments()
if err != nil || len(placed) == 0 {
return
}
@@ -654,9 +654,9 @@ func (v *Vault) LoginsPass() {
}
}
// NodeLeft is heard when a node's key of the placement is deleted: its key of node-logins goes with it.
// NodeLeft is heard when a node's key of the assignments is deleted: its key of node-logins goes with it.
func (v *Vault) NodeLeft(node string) {
if v.Logins == nil || v.Placement.Read == nil {
if v.Logins == nil || v.Assigner.Read == nil {
return
}
if _, _, found, err := v.nodePlaced(node); err != nil || found {
@@ -667,8 +667,8 @@ func (v *Vault) NodeLeft(node string) {
func (v *Vault) nodeLeft(node string) {
if err := v.Logins.Delete(node); err != nil {
v.log("%s left the placement, and its key of %s could not be deleted: %v", node, NodeLoginsState, err)
v.log("%s left the assignments, and its key of %s could not be deleted: %v", node, NodeLoginsState, err)
return
}
v.log("%s left the placement: its key of %s is deleted", node, NodeLoginsState)
v.log("%s left the assignments: its key of %s is deleted", node, NodeLoginsState)
}
+14 -14
View File
@@ -92,7 +92,7 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planRead
if c.Consumer != node {
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
}
// The consuming module stands on the machine: read from the placement and the assignments, never assumed (one
// The consuming module stands on the machine: read from the assignments and the assignments, never assumed (one
// that cannot be read refuses). The assignments' mirror can be behind, so one it does not hold is confirmed directly.
placed, err := plan.placedOn(c.ConsumerModule)
if err != nil {
@@ -102,7 +102,7 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planRead
return ""
}
raw, why, err := plan.read(c.ConsumerModule)
if why != "" && v.Placement.Read == nil {
if why != "" && v.Assigner.Read == nil {
return why
}
if why == "" {
@@ -130,18 +130,18 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planRead
// planReader reads the assignments of one machine's modules for one answer of `own`, each module's once (issue 1126,
// 2026-10-28: one read per own secret and per credential, 203 on novox, held the answer past the node-engine's bound),
// and the machine's placement once (issue 774, 2026-10-31: read once per credential, 143 bus reads in one answer to ace).
// and the machine's assignments once (issue 774, 2026-10-31: read once per credential, 143 bus reads in one answer to ace).
type planReader struct {
node string
reader AssignmentReader
confirm AssignmentReader
why string
got map[string]planRead
// placement reads the machine's placed modules; placed and placedErr keep the one read for the answer.
placement func() ([]string, error)
placedRead bool
placed []string
placedErr error
// assignments reads the machine's placed modules; placed and placedErr keep the one read for the answer.
assignments func() ([]string, error)
placedRead bool
placed []string
placedErr error
}
type planRead struct {
@@ -156,13 +156,13 @@ func (v *Vault) plansOf(node string) *planReader {
confirm = standing(d.Confirm)
}
return &planReader{node: node, confirm: confirm, reader: read, why: why, got: map[string]planRead{},
placement: func() ([]string, error) { return v.placedModules(node) }}
assignments: func() ([]string, error) { return v.placedModules(node) }}
}
// placedOn says module is placed on the machine by the catalogue, from the machine's placement read once for the answer.
// placedOn says module is placed on the machine by the catalogue, from the machine's assignments read once for the answer.
func (p *planReader) placedOn(module string) (bool, error) {
if !p.placedRead {
p.placed, p.placedErr = p.placement()
p.placed, p.placedErr = p.assignments()
p.placedRead = true
}
return slices.Contains(p.placed, module), p.placedErr
@@ -170,8 +170,8 @@ func (p *planReader) placedOn(module string) (bool, error) {
// placedModules is the machine's placed modules, read once for the answer.
func (p *planReader) placedModules() ([]string, error) {
if p.placement == nil {
return nil, errors.New("the machine's placement is not read here")
if p.assignments == nil {
return nil, errors.New("the machine's assignments is not read here")
}
p.placedOn("")
return p.placed, p.placedErr
@@ -458,7 +458,7 @@ func (v *Vault) HandedOf(node string) ([]HandedJudged, error) {
if placedErr != nil {
// Never dropped: an answer that leaves out the modules placed there, with no word of it, would read as "the others
// are not placed" (issue 1966 review note).
return nil, fmt.Errorf("the placement of %s could not be read, so the modules placed there are not all judged: %w", node, placedErr)
return nil, fmt.Errorf("the assignments of %s could not be read, so the modules placed there are not all judged: %w", node, placedErr)
}
for _, m := range placed {
modules[m] = true
@@ -146,10 +146,10 @@ func TestOwnReadsEachAssignmentOnceAndSyncsItsRecordOnce(t *testing.T) {
}
}
// Issue 774, 2026-10-31 (ace's 143 credentials went unanswered past the node-engine's 15 s while novox's asked every
// 8 s): the machine's placement, one key, is read once per answer too, never once per credential.
placed := placementsOf(t, map[string][]string{"anchor": {"web"}})
placementReads := 0
v.Placement = PlacementReader{Keys: placed.Keys, Read: func(key string) ([]byte, error) { placementReads++; return placed.Read(key) }}
// 8 s): the machine's assignments, one key, is read once per answer too, never once per credential.
placed := assignedNodes(t, map[string][]string{"anchor": {"web"}})
assignmentsReads := 0
v.Assigner = AssignerReader{Keys: placed.Keys, Read: func(key string) ([]byte, error) { assignmentsReads++; return placed.Read(key) }}
for k := range reads {
delete(reads, k)
}
@@ -168,10 +168,10 @@ func TestOwnReadsEachAssignmentOnceAndSyncsItsRecordOnce(t *testing.T) {
if len(reads) != 2 {
t.Errorf("the answer read %v, want each of anchor's two modules once", reads)
}
if placementReads != 1 {
t.Errorf("one answer of two credentials read anchor's placement %d times, want once", placementReads)
if assignmentsReads != 1 {
t.Errorf("one answer of two credentials read anchor's assignments %d times, want once", assignmentsReads)
}
v.Placement = PlacementReader{}
v.Assigner = AssignerReader{}
if n := v.Store.syncs - syncs; n != 1 {
t.Errorf("one answer synced the record %d times", n)
}
@@ -270,7 +270,7 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) {
}
v := newVault(t, nil)
v.Askers = Declared{Read: plans.read, Store: v.Store}
v.Placement = placementsOf(t, map[string][]string{"anchor": {"grafana", "web", "db"}})
v.Assigner = assignedNodes(t, map[string][]string{"anchor": {"grafana", "web", "db"}})
pub, _ := aSealingKey(t)
for _, m := range []MoveOf{
{Node: "anchor", Module: "grafana", Name: "admin", Origin: "made", Generation: "g-1", SealingKey: pub},
@@ -315,14 +315,14 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) {
if len(judged) != 4 {
t.Errorf("own_handed names %v, want builder, db, grafana and web", judged)
}
// Review note of issue 1966: a placement that cannot be read is an error the tool answers, never a list that leaves out
// Review note of issue 1966: an assignment that cannot be read is an error the tool answers, never a list that leaves out
// the modules placed there while the vault keeps an own secret of others.
v.Placement = PlacementReader{Keys: func() ([]string, error) { return nil, errors.New("the bus did not answer") },
v.Assigner = AssignerReader{Keys: func() ([]string, error) { return nil, errors.New("the bus did not answer") },
Read: func(string) ([]byte, error) { return nil, errors.New("the bus did not answer") }}
if got, err := v.HandedOf("anchor"); err == nil || !strings.Contains(err.Error(), "placement of anchor could not be read") || got != nil {
t.Errorf("an unreadable placement answered %v, %v", got, err)
if got, err := v.HandedOf("anchor"); err == nil || !strings.Contains(err.Error(), "assignments of anchor could not be read") || got != nil {
t.Errorf("an unreadable assignments answered %v, %v", got, err)
}
if _, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "own_handed").Run(map[string]any{"node": "anchor"}); err == nil {
t.Error("own_handed answered with the placement unread")
t.Error("own_handed answered with the assignments unread")
}
}
+1 -1
View File
@@ -254,7 +254,7 @@ type Drift struct {
Note string `json:"note"`
}
// OwnDrift reads every node's own have key (`<node>.node`) and lists, from its latest answers to the placement
// OwnDrift reads every node's own have key (`<node>.node`) and lists, from its latest answers to the assignments
// candidates, every own secret whose shown bodies say another value (vaultclient.OwnValuesDiffer), with what the vault holds
// of it and whether move-in with replace_running would take the running value now.
func (v *Vault) OwnDrift() (Drift, error) {
+35 -46
View File
@@ -1,89 +1,78 @@
package main
// Where modules stand, from the placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774), the seat mesh-module-assigner's
// state held by the assigner (ADR 0567). The controller's assignments mirror goes with its lease, so the vault reads which
// module stands on which node from the seat's placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
// which only an assignment's value carries, is read from the assignments still: its home after the lease is a subtask of
// its own (issue 774).
// Where modules stand, from the seat mesh-module-assigner's state assignments (novox/hq ADR 0549 §3, §5; ADR 0561;
// issue 774), held by the assigner (ADR 0567). The controller's mirror of bindings (Vault.Assignments) goes with its lease,
// so the vault reads which module stands on which node from the seat's assignments first, and the mirror beside it while
// both stand: a module either names stands. The provisioner's plan, which only a binding's value carries, is read from
// the mirror still: its home after the lease is a subtask of its own (issue 774).
import (
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"slices"
"sort"
"strings"
)
// PlacementReader reads the seat's placement: every key, and one key's value (nil when there is none).
type PlacementReader struct {
// AssignerReader reads the seat's assignments: every key, and one key's value (nil when there is none).
type AssignerReader struct {
Keys func() ([]string, error)
Read func(key string) ([]byte, error)
}
// placements is every node's placement, by node, from the state's keys in either form (assignerclient.PlacementsOf; research
// 072 row 37); none when the vault reads no placement. A node whose keys do not read whole is read as they stand.
func (v *Vault) placements() (map[string]placing.Placement, error) {
out := map[string]placing.Placement{}
if v.Placement.Keys == nil || v.Placement.Read == nil {
// assignments is every node's assignments, by node, from the state's keys (assignerclient.AssignmentsOf; research
// 072 row 37); none when the vault reads no assignments. A node whose keys do not read whole is read as they stand.
func (v *Vault) assignments() (map[string]placing.Assignments, error) {
out := map[string]placing.Assignments{}
if v.Assigner.Keys == nil || v.Assigner.Read == nil {
return out, nil
}
keys, err := v.Placement.Keys()
keys, err := v.Assigner.Keys()
if err != nil {
return nil, fmt.Errorf("the placement's keys could not be read: %w", err)
return nil, fmt.Errorf("the mirror's keys could not be read: %w", err)
}
values := map[string][]byte{}
for _, k := range keys {
if _, _, _, err := assignerclient.ParseKey(k); err != nil {
continue
}
raw, err := v.Placement.Read(k)
raw, err := v.Assigner.Read(k)
if err != nil {
return nil, fmt.Errorf("the placement's key %s could not be read: %w", k, err)
return nil, fmt.Errorf("the assignments' key %s could not be read: %w", k, err)
}
if raw != nil {
values[k] = raw
}
}
out, _ = assignerclient.PlacementsOf(values)
out, _ = assignerclient.AssignmentsOf(values)
return out, nil
}
// nodePlaced is what node's own key of the placement says: the modules placed there and what a person decided of it,
// read from its node key `<node>.node` alone (which names every module placed, research 072 row 37), or, for a node not
// converted yet, from its key in the form before, `placement.<node>`. found is false when it has neither.
// nodePlaced is what node's own key of the assignments says: the modules placed there and what a person decided of it,
// read from its node key `<node>.node` alone (which names every module placed, research 072 row 37). found is false when
// it has none.
func (v *Vault) nodePlaced(node string) (modules []string, decided placing.NodeDecided, found bool, err error) {
key, err := assignerclient.NodeKey(node)
if err != nil {
return nil, decided, false, err
}
raw, err := v.Placement.Read(key)
raw, err := v.Assigner.Read(key)
if err != nil {
return nil, decided, false, err
}
if raw != nil {
n, err := assignerclient.DecodeNode(raw)
if err != nil {
return nil, decided, false, fmt.Errorf("the placement of %s is not readable: %w", node, err)
return nil, decided, false, fmt.Errorf("the assignments of %s are not readable: %w", node, err)
}
for _, r := range n.Modules {
modules = append(modules, r.Module)
}
return modules, n.Decided, true, nil
}
legacy, err := assignerclient.LegacyKey(node)
if err != nil {
return nil, decided, false, err
}
if raw, err = v.Placement.Read(legacy); err != nil || raw == nil {
return nil, decided, false, err
}
p, err := assignerclient.DecodePlacement(raw)
if err != nil {
return nil, decided, false, fmt.Errorf("the placement of %s is not readable: %w", node, err)
}
return assignerclient.Assigned(p), p.Decided, true, nil
return nil, decided, false, nil
}
// placedOn says module is placed on node by the catalogue.
@@ -92,17 +81,17 @@ func (v *Vault) placedOn(node, module string) (bool, error) {
return slices.Contains(placed, module), err
}
// placedModules is every module the catalogue places on node, read from the node's own key of the placement
// (nodePlaced); none when the vault reads no placement or the node has no key.
// placedModules is every module the catalogue places on node, read from the node's own key of the assignments
// (nodePlaced); none when the vault reads no assignments or the node has no key.
func (v *Vault) placedModules(node string) ([]string, error) {
if v.Placement.Read == nil {
if v.Assigner.Read == nil {
return nil, nil
}
modules, _, _, err := v.nodePlaced(node)
return modules, err
}
// standsOn says module stands on node: placed there, or assigned there while the assignments stand. A read that fails is
// standsOn says module stands on node: placed there, or bound there while the mirror stands. A read that fails is
// an error, never "not there".
func (v *Vault) standsOn(node, module string) (bool, error) {
placed, err := v.placedOn(node, module)
@@ -111,8 +100,8 @@ func (v *Vault) standsOn(node, module string) (bool, error) {
}
d, ok := v.Askers.(Declared)
if !ok || d.Read == nil {
if v.Placement.Read == nil {
return false, errors.New("the vault reads neither the placement nor the assignments")
if v.Assigner.Read == nil {
return false, errors.New("the vault reads neither the seat's assignments nor the mirror")
}
return false, nil
}
@@ -120,10 +109,10 @@ func (v *Vault) standsOn(node, module string) (bool, error) {
return raw != nil, err
}
// busNode is the node running the bus's module: from the placement, and from the assignments while no placement names
// busNode is the node running the bus's module: from the seat's assignments, and from the mirror while no assignment names
// one. The vault adopts from one node alone.
func (v *Vault) busNode() (string, error) {
placed, err := v.placements()
placed, err := v.assignments()
if err != nil {
return "", err
}
@@ -138,7 +127,7 @@ func (v *Vault) busNode() (string, error) {
from = "assigned"
keys, err := v.Assignments.Keys()
if err != nil {
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
return "", fmt.Errorf("the mirror's keys could not be read: %w", err)
}
for _, k := range keys {
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
@@ -146,7 +135,7 @@ func (v *Vault) busNode() (string, error) {
}
}
} else if len(nodes) == 0 && len(placed) == 0 {
return "", errors.New("the vault reads neither the placement nor the assignments, so it cannot tell which node holds the bus")
return "", errors.New("the vault reads neither the seat's assignments nor the mirror, so it cannot tell which node holds the bus")
}
sort.Strings(nodes)
switch len(nodes) {
@@ -7,22 +7,22 @@ import (
"strings"
"testing"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
)
// placementsOf is the seat mesh-module-assigner's placement of each node, as the vault reads it.
func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
// assignedNodes is the seat mesh-module-assigner's assignments of each node, as the vault reads it.
func assignedNodes(t *testing.T, nodes map[string][]string) AssignerReader {
t.Helper()
values := map[string][]byte{}
for node, modules := range nodes {
p := placing.Placement{V: vaultclient.Version, Node: node}
p := placing.Assignments{V: vaultclient.Version, Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, placing.PlacedModule{Module: m})
}
// Research 072 row 37: the node key and a key per module.
kv, err := assignerclient.PlacementValues(p)
kv, err := assignerclient.AssignmentsValues(p)
if err != nil {
t.Fatal(err)
}
@@ -30,7 +30,7 @@ func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
values[k] = raw
}
}
return PlacementReader{
return AssignerReader{
Keys: func() ([]string, error) {
var out []string
for k := range values {
@@ -43,30 +43,30 @@ func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
}
// 774/AC11 (novox/hq ADR 0561, ADR 0549 §3, §5): with no assignments to read, the vault tells from the seat's
// placement which node holds the bus and whether a consumer's module stands on its node; while both stand, a module
// assignments which node holds the bus and whether a consumer's module stands on its node; while both stand, a module
// either names stands.
func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
func TestTheVaultReadsWhereModulesStandFromAssignments(t *testing.T) {
v := newVault(t, nil)
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats", "web"}, "ace": {"shop"}})
v.Assigner = assignedNodes(t, map[string][]string{"novox": {"nats", "web"}, "ace": {"shop"}})
if node, err := v.busNode(); err != nil || node != "novox" {
t.Errorf("the bus node from placement: %q, %v", node, err)
t.Errorf("the bus node from assignments: %q, %v", node, err)
}
asked := func(node, module string) CredentialAsked {
return CredentialAsked{Role: "consumer", Provision: "postgres", Consumer: node, ConsumerModule: module, Provider: "novox"}
}
if why := v.credentialRefusal("ace", asked("ace", "shop"), v.plansOf("ace")); why != "" {
t.Errorf("shop stands on ace by placement, refused: %s", why)
t.Errorf("shop stands on ace by assignments, refused: %s", why)
}
if why := v.credentialRefusal("ace", asked("ace", "web"), v.plansOf("ace")); !strings.Contains(why, "is not on ace") {
t.Errorf("web is not placed on ace, and is answered %q", why)
}
// While both stand, an assignment still names a module placement does not yet.
// While both stand, an assignment still names a module assignments does not yet.
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
if why := v.credentialRefusal("ace", asked("ace", "web"), v.plansOf("ace")); why != "" {
t.Errorf("web assigned on ace, refused: %s", why)
}
// Two nodes placed with the bus: the vault adopts from neither.
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "ace": {"nats"}})
v.Assigner = assignedNodes(t, map[string][]string{"novox": {"nats"}, "ace": {"nats"}})
if _, err := v.busNode(); err == nil || !strings.Contains(err.Error(), "2 nodes") {
t.Errorf("two bus nodes: %v", err)
}
@@ -74,7 +74,7 @@ func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
// The assigner's tool and views family the vault names are the ones its manifest invokes and consumes; no client names a
// holder (novox/hq ADR 0625 §4).
func TestThePlacementNamesAreTheManifests(t *testing.T) {
func TestTheAssignmentsNamesAreTheManifests(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
@@ -95,7 +95,7 @@ func TestThePlacementNamesAreTheManifests(t *testing.T) {
t.Errorf("the vault reads %s, its manifest's reads are %v", r, reads.Reads)
}
}
if !slices.Contains(m.Invokes, PlacementTool) || !slices.Contains(m.Consumes, PlacementViewsEvent) {
t.Errorf("the vault asks %s and follows %s, its manifest invokes %v and consumes %v", PlacementTool, PlacementViewsEvent, m.Invokes, m.Consumes)
if !slices.Contains(m.Invokes, AssignmentsTool) || !slices.Contains(m.Consumes, AssignmentsViewsEvent) {
t.Errorf("the vault asks %s and follows %s, its manifest invokes %v and consumes %v", AssignmentsTool, AssignmentsViewsEvent, m.Invokes, m.Consumes)
}
}
@@ -15,11 +15,11 @@ import (
"encoding/base64"
"encoding/json"
"errors"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"os"
"os/exec"
"path/filepath"
@@ -58,7 +58,7 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
t.Fatal(err)
}
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts",
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_assignments"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
t.Fatal(err)
}
@@ -92,7 +92,7 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
}
return map[string]any{"acts": []HandActSeen{{ID: "act-1", By: "person.jochen", At: time.Now(), Verb: "rotate-node", Args: []string{"g14"}}}}, nil
}
p.followPlacement()
p.followAssignments()
go p.serve(stdout)
oldPassword := "the-old-password-of-g14"
@@ -160,21 +160,21 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
reload()
// The bus node (novox, running nats) and the node that rotates (g14); no lease key anywhere (issue 1126: the controller
// ended by its unassignment); g14's login adopted from
// the loaded list, its password known to the engine. Written after the vault's first view of the placement, so they
// the loaded list, its password known to the engine. Written after the vault's first view of the assignments, so they
// reach it only as the owner's views events, and after the bus node's have: the vault heard that have while no node
// held the bus, and adopts from it (learning the loaded list's revision) when the placement names novox.
// held the bus, and adopts from it (learning the loaded list's revision) when the assignments names novox.
p.firstViewAnswered()
for node, modules := range map[string][]string{"novox": {"nats"}, "g14": {"web"}} {
pl := placing.Placement{V: vaultclient.Version, Node: node}
pl := placing.Assignments{V: vaultclient.Version, Node: node}
for _, m := range modules {
pl.Modules = append(pl.Modules, placing.PlacedModule{Module: m})
}
values, err := assignerclient.PlacementValues(pl)
values, err := assignerclient.AssignmentsValues(pl)
if err != nil {
t.Fatal(err)
}
for k, raw := range values {
kv, _ := p.kv("mesh-delivery-assigner.placement")
kv, _ := p.kv("mesh-delivery-assigner.assignments")
if _, err := kv.Put(ctx, k, raw); err != nil {
t.Fatal(err)
}
+9 -9
View File
@@ -3,7 +3,7 @@ package main
// The vault signs (novox/hq ADR 0550 §2; issue 774, design 47's L3 item d). It makes an ed25519 signing key and an x509
// authority once, keeps each as its own secret with its operator copy (ADR 0113 §22), publishes their public halves in its
// state `signers`, and signs with them on its verb `sign`: a node's certificate for that node's node-engine alone, built
// from bus state (the name from the node's placement, the serving key from its record), never from bytes the caller chose.
// from bus state (the name from the node's assignments, the serving key from its record), never from bytes the caller chose.
// Each signature is said as `signed` and kept in the record of hand-outs, never a key. The controller's key is never
// copied here.
@@ -295,7 +295,7 @@ func (v *Vault) redeeming(node string) error {
// signRedeemingCertificate signs the certificate of a machine that is joining, for the seat mesh-node-enroller at the redeem
// (ADR 0587 §6). The machine has written no record yet, so the serving key is the one it presented at the redeem, which
// counts only when the four keys presented have the digest node-account recorded for its account; the name is its
// placement's, which mesh-enrolment writes before it asks.
// the assignments', which mesh-enrolment writes before it asks.
func (v *Vault) signRedeemingCertificate(caller, node string, keys PresentedKeys) (meshvault.SignAnswer, error) {
if err := v.redeeming(node); err != nil {
return meshvault.SignAnswer{}, err
@@ -368,7 +368,7 @@ func (v *Vault) signNodeCertificate(caller, asked string) (meshvault.SignAnswer,
return v.signCertificateFor(caller, node)
}
// signCertificateFor signs node's certificate from its placement's name and its record's serving key.
// signCertificateFor signs node's certificate from its the assignments' name and its record's serving key.
func (v *Vault) signCertificateFor(caller, node string) (meshvault.SignAnswer, error) {
name, err := v.internalName(node)
if err != nil {
@@ -401,21 +401,21 @@ func (v *Vault) signCertificateWith(caller, node, name string, serving ed25519.P
return answer, nil
}
// internalName is node's name inside the mesh: only a node with an address on the private network in its placement has
// internalName is node's name inside the mesh: only a node with an address on the private network in its assignments has
// one (a certificate for a name nothing resolves is one nothing can check).
func (v *Vault) internalName(node string) (string, error) {
if v.Placement.Read == nil {
return "", errors.New("the vault reads no placement, so it cannot tell a node's internal name")
if v.Assigner.Read == nil {
return "", errors.New("the vault reads no assignments, so it cannot tell a node's internal name")
}
_, decided, found, err := v.nodePlaced(node)
if err != nil {
return "", fmt.Errorf(notYetPrefix+"the placement of %s could not be read: %w", node, err)
return "", fmt.Errorf(notYetPrefix+"the assignments of %s could not be read: %w", node, err)
}
if !found {
return "", fmt.Errorf("%s has no placement of the seat mesh-module-assigner, so it has no name inside the mesh", node)
return "", fmt.Errorf("%s has no assignments of the seat mesh-module-assigner, so it has no name inside the mesh", node)
}
if decided.Address == "" {
return "", fmt.Errorf("%s is not on the private network (its placement names no address), so it has no name inside the mesh to be certified for", node)
return "", fmt.Errorf("%s is not on the private network (its assignments names no address), so it has no name inside the mesh to be certified for", node)
}
return overlay.InternalName(node), nil
}
@@ -5,12 +5,12 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/json"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
placing "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"strings"
"testing"
@@ -33,32 +33,29 @@ func (s *signersState) Put(key string, value []byte) error {
// L3 s2 to s5 (novox/hq ADR 0550 §2): the vault makes its signing key and authority once, with their operator copies and
// no other holder; publishes their public halves only when they differ; signs a node's certificate for that node's
// node-engine alone, from its placement's name and its record's serving key; and no answer, event or record carries a
// node-engine alone, from its the assignments' name and its record's serving key; and no answer, event or record carries a
// key.
func TestTheVaultSigns(t *testing.T) {
v := newVault(t, nil)
state := &signersState{keys: map[string][]byte{}}
v.Signers = state
servingPub, _, _ := ed25519.GenerateKey(rand.Reader)
placements := map[string]placing.Placement{
assignments := map[string]placing.Assignments{
"ace": {Node: "ace", Decided: placing.NodeDecided{Address: "10.77.0.3"}},
"g14": {Node: "g14", Decided: placing.NodeDecided{Address: "10.77.0.4"}},
"plain": {Node: "plain"},
}
// Research 072 row 37: ace and plain in their keys per module and node, g14 still in the form before.
v.Placement = PlacementReader{Read: func(key string) ([]byte, error) {
node, _, kind, err := assignerclient.ParseKey(key)
// Research 072 row 37: every node in its keys per module and node.
v.Assigner = AssignerReader{Read: func(key string) ([]byte, error) {
node, _, _, err := assignerclient.ParseKey(key)
if err != nil {
return nil, err
}
p, ok := placements[node]
if !ok || (node == "g14") != (kind == assignerclient.LegacyKind) {
p, ok := assignments[node]
if !ok {
return nil, nil
}
if kind == assignerclient.LegacyKind {
return assignerclient.EncodePlacement(p)
}
values, err := assignerclient.PlacementValues(p)
values, err := assignerclient.AssignmentsValues(p)
return values[key], err
}}
records := map[string]string{"ace": base64.StdEncoding.EncodeToString(servingPub), "plain": base64.StdEncoding.EncodeToString(servingPub)}
@@ -319,27 +319,27 @@ func TestSecretOwnListsFingerprintsOfOwnSecretsOnly(t *testing.T) {
if _, err := v.AdoptFrom("ace", have(), "the test", true); err == nil || !strings.Contains(err.Error(), "does not hold the bus") {
t.Errorf("a handing from a node not holding the bus: %v", err)
}
// A have heard while the placement names no node holding the bus adopts nothing then; it is kept, and adopted when
// the placement next changes to name its node, once (the placement's event, no timer).
// A have heard while the assignments names no node holding the bus adopts nothing then; it is kept, and adopted when
// the assignments next changes to name its node, once (the assignments' event, no timer).
assignedKeys := v.Assignments
v.Assignments = Mirror{Keys: func() ([]string, error) { return []string{"ace.dunst"}, nil }}
handing["node.ace"] = h("k")
if _, err := v.HaveHeard("novox", have(), "the watch"); err == nil || !strings.Contains(err.Error(), "no node is") {
t.Fatalf("a have heard before the placement names the bus node: %v", err)
t.Fatalf("a have heard before the assignments names the bus node: %v", err)
}
v.PlacementChanged()
v.AssignmentsChanged()
if acc.keys["node.ace"].Hash != h("m") {
t.Fatalf("adopted while no node holds the bus: %+v", acc.keys["node.ace"])
}
v.Assignments = assignedKeys
before := len(v.bus.events)
v.PlacementChanged()
v.AssignmentsChanged()
if acc.keys["node.ace"].Hash != h("k") || eventsNamed(v, before, AccountHashedEvent) != 1 {
t.Fatalf("the kept have is not adopted when the placement names its node: %+v", acc.keys["node.ace"])
t.Fatalf("the kept have is not adopted when the assignments names its node: %+v", acc.keys["node.ace"])
}
v.PlacementChanged()
v.AssignmentsChanged()
if eventsNamed(v, before, AccountHashedEvent) != 1 {
t.Errorf("the kept have was adopted again at a later placement change")
t.Errorf("the kept have was adopted again at a later assignments change")
}
listed, err := own.Run(map[string]any{})
if err != nil {
+3 -3
View File
@@ -66,12 +66,12 @@ type Vault struct {
// adoption is the last adoption (adopt.go), under mu.
adoption adoptionKept
// heard is each node's latest have of the bus module the watch handed (adopt.go), under mu: one not adopted is
// adopted when the placement next changes.
// adopted when the assignments next changes.
heard map[string]heardHave
// WantRead reads one wanted account afresh (issue-account): nil reads the wants as the watch handed them.
WantRead func(user string) ([]byte, bool, error)
// Placement reads the seat's placement (placed.go): where modules stand once the assignments are gone.
Placement PlacementReader
// Assignments reads the seat's assignments (placed.go): where modules stand once the assignments are gone.
Assigner AssignerReader
// minting is what the vault knows of the wanted bus accounts (mint.go).
minting minting
// Signers is the vault's state `signers`, where it publishes its signing halves (signing.go, ADR 0550 §2): nil writes
@@ -137,7 +137,7 @@ func filesUnder(t *testing.T, dir string) map[string]string {
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
// seat mesh-provisioner's plans, for what each node's plan names (ADR 0570 clause 2), (askers.go, design 57 V4), the node-engines' haves, where
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted
// accounts (mint.go, ADR 0561 §2), and the seat's placement (placed.go), in
// accounts (mint.go, ADR 0561 §2), and the seat's assignments (placed.go), in
// main.go alone.
func TestNoBucketHoldsASecret(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
@@ -155,7 +155,7 @@ func TestNoBucketHoldsASecret(t *testing.T) {
}
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-provisioner.plans","mesh-provisioner.provisions"]` {
t.Errorf("the manifest reads %s: the vault reads the seat mesh-provisioner's plans and provisions, the haves, the "+
"grants part's wanted accounts alone (the placement is the assigner's, through its tool and events, ADR 0615), never the controller's lease (issue 1126)", r)
"grants part's wanted accounts alone (the assignments is the assigner's, through its tool and events, ADR 0615), never the controller's lease (issue 1126)", r)
}
sources, _ := filepath.Glob("*.go")
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|\bKeyValue\b`)
+2 -2
View File
@@ -13,8 +13,8 @@ require (
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
+2 -2
View File
@@ -32,7 +32,7 @@
],
"invokes": [
"seat:mesh-hand-acts.hand-acts",
"mesh-delivery-assigner.assigner_read_placement"
"mesh-delivery-assigner.assigner_read_assignments"
],
"reads": [
"node-engine.have",
@@ -253,6 +253,6 @@
}
},
"consumes": [
"mesh-delivery-assigner.views.placement"
"mesh-delivery-assigner.views.assignments"
]
}
+1 -1
View File
@@ -4,7 +4,7 @@ The watcher's watcher (novox/hq to-be 45 §5, signal S10, ADR 0227 rule 6): what
when the parts that would tell them are what failed.
- **Assigned to one machine that is not the control node.** It reads where the bus runs (the seat
`mesh-broker`'s holders, from the placement seat's state of who holds which seat) every ten minutes;
`mesh-broker`'s holders, from the assignments seat's state of who holds which seat) every ten minutes;
on the same machine its status says `MISPLACED`.
- **Watches two signals.**
- **The self-check:** the seat `mesh-self-check`'s `checked` event.
+12 -12
View File
@@ -10,8 +10,8 @@ import (
"context"
"encoding/json"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"os"
"strings"
"sync"
@@ -76,10 +76,10 @@ func main() {
node := os.Getenv("MESH_NODE")
w := NewWatcher(tg, time.Now, logf, node)
l := &listening{now: "not yet: starting"}
placement := &views.Reader{Module: "mesh-watcher", Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
placement.Follow(assignerModule, mesh.StateSeatHolders, SeatHoldersTool)
go run(w, l, node, placement)
if err := stdio.Serve("", append(tools(w, l), placement.Tools("watcher")...)); err != nil {
assignments := &views.Reader{Module: "mesh-watcher", Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
assignments.Follow(assignerModule, mesh.StateSeatHolders, SeatHoldersTool)
go run(w, l, node, assignments)
if err := stdio.Serve("", append(tools(w, l), assignments.Tools("watcher")...)); err != nil {
logf("%v", err)
os.Exit(1)
}
@@ -87,7 +87,7 @@ func main() {
// run keeps time, makes the round trips, reads where the controller is, and listens for heartbeats.
// The clock runs whatever the bus does: it is what notices the bus is gone.
func run(w *Watcher, l *listening, node string, placement *views.Reader) {
func run(w *Watcher, l *listening, node string, assignments *views.Reader) {
if err := w.Telegram.Ready(); err != nil {
logf("[mesh-watcher] BLIND until given: %v", err)
}
@@ -100,14 +100,14 @@ func run(w *Watcher, l *listening, node string, placement *views.Reader) {
// Where the bus runs: the assigner's seat-holders, asked once and then followed by its events (ADR 0615), never read
// from its state and never on a timer.
placed := func() {
if nodes, err := controlNodes(placement); err == nil {
if nodes, err := controlNodes(assignments); err == nil {
w.Placed(nodes)
} else {
logf("[mesh-watcher] cannot read where the bus runs (%v)", err)
}
}
placement.Changed = func(string, string, json.RawMessage, bool) { placed() }
placement.Start(context.Background())
assignments.Changed = func(string, string, json.RawMessage, bool) { placed() }
assignments.Start(context.Background())
handle := func(e stdio.Envelope) error {
if !isHeartbeat(e.Key) {
return nil
@@ -162,7 +162,7 @@ const assignerModule = "mesh-delivery-assigner"
const SeatHoldersTool = assignerModule + ".assigner_read_seat_holders"
// controlNodes reads which machines hold the bus, from the placement seat's state of who holds which seat (novox/hq
// controlNodes reads which machines hold the bus, from the assignments seat's state of who holds which seat (novox/hq
// ADR 0571 clause 4). The controller is not placed (ADR 0590), so the bus's machines are the control node.
func controlNodes(seats *views.Reader) ([]string, error) {
keys, err := seats.Values(assignerModule + "." + mesh.StateSeatHolders)
@@ -189,7 +189,7 @@ func holderNodes(keys map[string][]byte, seats ...string) ([]string, error) {
}
}
if len(out) == 0 {
return nil, fmt.Errorf("the placement seat's state names no holder of %s", strings.Join(seats, " or "))
return nil, fmt.Errorf("the assignments seat's state names no holder of %s", strings.Join(seats, " or "))
}
return out, nil
}
@@ -2,7 +2,7 @@ package main
import (
"encoding/json"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"os"
"path/filepath"
"reflect"
@@ -213,7 +213,7 @@ func TestOnTheControlNodeItSaysItIsMisplaced(t *testing.T) {
}
}
func TestTheHoldersAreReadFromThePlacementSeatsState(t *testing.T) {
func TestTheHoldersAreReadFromTheAssignmentsSeatsState(t *testing.T) {
keys := map[string][]byte{
"mesh-broker": []byte(`{"v":1,"seat":"mesh-broker","scope":"mesh","holders":[{"module":"nats","node":"n1"},{"module":"nats","node":"n2"},{"module":"nats","node":"n1"}]}`),
"git": []byte(`{"v":1,"seat":"git","scope":"mesh","holders":[{"module":"gitea","node":"n3"}]}`),
+2 -2
View File
@@ -5,8 +5,8 @@ go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
require (
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
github.com/klauspost/compress v1.20.1 // indirect
+1 -1
View File
@@ -12,7 +12,7 @@ package main
import (
"context"
"fmt"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"os"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
@@ -2,7 +2,7 @@ package main
import (
"encoding/json"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
mesh "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"os"
"strings"
"testing"
@@ -21,7 +21,7 @@ func manifest(t *testing.T) map[string]any {
return m
}
// 1516/AC6: minio claims object-store (scope mesh) serving link, reads the placement seat's seat-holders, builds the Go
// 1516/AC6: minio claims object-store (scope mesh) serving link, reads the assignments seat's seat-holders, builds the Go
// bundle, and no tool named minio_presigned_url remains.
func TestManifestClaimsTheSeat(t *testing.T) {
m := manifest(t)
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"os"
"path/filepath"
"strings"
@@ -58,7 +58,7 @@ func linkKeyID(bucket string) string {
type server struct {
configPath, linksDir, receives string
now func() time.Time
// seats is the picture of the placement seat's state of who holds which seat, every key: asked of the assigner once
// seats is the picture of the assignments seat's state of who holds which seat, every key: asked of the assigner once
// and followed by its events (ADR 0615); a test replaces it.
seats func() (map[string][]byte, error)
}
@@ -197,7 +197,7 @@ func (s *server) allowed(g grant, node, module, method string) error {
if seat, isSeat := strings.CutPrefix(sh.To, "seat:"); isSeat {
held, err := s.holds(seat, node, module)
if err != nil {
return false, fmt.Errorf("a share names seat %s and the placement seat's holders cannot be read to tell whether this caller holds it: %w", seat, err)
return false, fmt.Errorf("a share names seat %s and the assignments seat's holders cannot be read to tell whether this caller holds it: %w", seat, err)
}
return held, nil
}
@@ -240,7 +240,7 @@ func has(list []string, v string) bool {
return false
}
// holds says whether node.module holds the seat, from the placement seat's state of who holds which seat (novox/hq ADR
// holds says whether node.module holds the seat, from the assignments seat's state of who holds which seat (novox/hq ADR
// 0571 clause 4), from the picture the assigner's first view and events keep. No first view yet is refused, saying so.
func (s *server) holds(seat, node, module string) (bool, error) {
keys, err := s.seats()

Some files were not shown because too many files have changed in this diff Show More