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:
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
} }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
+38
-38
@@ -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)
|
||||
}},
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"}]}`),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user