Merge branch 'feat/modules-name-no-node' into ground-1838

This commit is contained in:
jochen
2026-10-22 21:35:25 +02:00
23 changed files with 208 additions and 208 deletions
+2 -2
View File
@@ -52,7 +52,7 @@ through an injected runner and a fake root in the tests.
## What changes when it is assigned
| | g14 | shanks |
| | node-b | node-d |
|---|---|---|
| package | none: `blueman` 2.4.6 is installed, explicitly, from `extra` | the same |
| start | none: dex starts the applet from the package's entry, in the login session's scope; the tray icon with it | none on disk. **The applet running now came from the predecessor's window-manager line** (a child of i3, since the session of 2026-10-04 16:00). That session began before the `i3` module dropped the line and installed `dex`, so the next login is the first that starts it from the entry |
@@ -62,7 +62,7 @@ The workstations are already in the state this module describes.
## Migration (ADR 0182)
Nothing is required on either machine. On shanks, log out and in once, or run `blueman_restart`, and
Nothing is required on either machine. On node-d, log out and in once, or run `blueman_restart`, and
the applet runs from its one start. `blueman_check` then answers `ok`.
## Leaves as found
+2 -2
View File
@@ -99,8 +99,8 @@ On the laptop:
1. `systemctl --user disable --now mem-guard.service`, then delete
`~/.config/systemd/user/mem-guard.service` and `~/scripts/mem-guard.sh`.
2. `/etc/systemd/oomd.conf.d/g14-oomd.conf`: the same values as the module's drop-in.
3. `/etc/sysctl.d/90-g14-zram.conf`: the same key as the module's file.
2. `/etc/systemd/oomd.conf.d/node-b-oomd.conf`: the same values as the module's drop-in.
3. `/etc/sysctl.d/90-node-b-zram.conf`: the same key as the module's file.
The desktop had none of these.
@@ -4,14 +4,14 @@ import "testing"
// TestSendKeyNamesThisMachine proves Send asks its own machine's node-notifier, not the bare seat: a bare
// "seat:node-notifier.send" is answered by whichever machine's node-notifier takes it first, so notices sent
// from g14 and shanks were lost to "503 no responders" or another machine's desktop (novox/hq issue 865).
// from node-b and node-d were lost to "503 no responders" or another machine's desktop (novox/hq issue 865).
// desk-channel names the machine it asks the same way ("seat:node-notifier.send@"+machine,
// modules/desk-channel/cmd/desk-channel/main.go); sendKey must read this machine's own name the same way the
// SDK's own Emit does, from MESH_NODE, which the runtime sets.
func TestSendKeyNamesThisMachine(t *testing.T) {
t.Setenv("MESH_NODE", "g14")
t.Setenv("MESH_NODE", "node-b")
if got, want := sendKey(), "seat:node-notifier.send@g14"; got != want {
if got, want := sendKey(), "seat:node-notifier.send@node-b"; got != want {
t.Fatalf("sendKey() = %q, want %q (it must name this machine, as desk-channel does)", got, want)
}
}
@@ -26,11 +26,11 @@ func (f *scriptedSecrets) Get(context.Context, string) (string, int, error) {
// that holds the value later fills the file with no change of the plan.
func TestARefusedSecretIsAskedAgainAfterRefusedForAndFilledWhenTheVaultHasIt(t *testing.T) {
now := time.Date(2026, 10, 16, 16, 20, 0, 0, time.UTC)
fake := &scriptedSecrets{errs: []error{errors.New("the vault refused it: the assignment g14.claude-code could not be read")}}
c := &Client{Node: "g14", Secrets: fake, Now: func() time.Time { return now }}
fake := &scriptedSecrets{errs: []error{errors.New("the vault refused it: the assignment node-b.claude-code could not be read")}}
c := &Client{Node: "node-b", Secrets: fake, Now: func() time.Time { return now }}
s := &state{secrets: map[string]heldSecret{}}
n := Need{Kind: "secret", Name: "gitea-read"}
name := "provision/novox/gitea-read/g14/claude-code"
name := "provision/node-c/gitea-read/node-b/claude-code"
if _, _, fail := c.vaultValue(context.Background(), s, n, name); fail == "" {
t.Fatal("a refusal did not fail the need")
@@ -50,7 +50,7 @@ func TestANotYetSecretIsAskedAgainOnEveryPass(t *testing.T) {
now := time.Date(2026, 10, 16, 16, 20, 0, 0, time.UTC)
notYet := fmt.Errorf("%w: the vault holds no secret named x yet", errNotYet)
fake := &scriptedSecrets{errs: []error{notYet, notYet}}
c := &Client{Node: "g14", Secrets: fake, Now: func() time.Time { return now }}
c := &Client{Node: "node-b", Secrets: fake, Now: func() time.Time { return now }}
s := &state{secrets: map[string]heldSecret{}}
n := Need{Kind: "secret", Name: "gitea-read"}
for i := 0; i < 2; i++ {
@@ -16,7 +16,7 @@ import (
// the module does not declare, a module not placed on the machine, a secret and a malformed ask are refused, each
// with its reason.
// toolRunner is g14's tool runner on the bus, asking as its account may.
// toolRunner is node-b's tool runner on the bus, asking as its account may.
type toolRunner struct {
t *testing.T
nc *nats.Conn
@@ -121,7 +121,7 @@ func TestNatsAModulesCodeReadsItsConfigAndIsToldOfAChange(t *testing.T) {
// Taken off the machine: said, and no longer answered.
b.del(t, AssignmentsBucket, node+".claude-code")
until(t, heard, `claude-code {"kind":"config"}`)
if a := runner.askConfig("claude-code", "managed_settings"); !strings.Contains(a.Error, "not placed on g14") {
if a := runner.askConfig("claude-code", "managed_settings"); !strings.Contains(a.Error, "not placed on node-b") {
t.Fatalf("a module taken off the machine was answered %+v", a)
}
}
@@ -138,7 +138,7 @@ func TestNatsWhatAModulesCodeMayNotReadIsRefusedWithTheReason(t *testing.T) {
for _, c := range []struct{ ask, refused string }{
{`{"module":"claude-code","kind":"config","key":"no_such_key"}`, "declares no config value no_such_key"},
{`{"module":"dunst","kind":"config","key":"font-size"}`, "dunst is not placed on g14"},
{`{"module":"dunst","kind":"config","key":"font-size"}`, "dunst is not placed on node-b"},
{`{"module":"claude-code","kind":"secret","key":"gitea-read"}`, "not answered on the tool runner's word"},
{`{"module":"claude-code","kind":"bound","key":"mcp-endpoint:url"}`, "not answered yet"},
{`{"module":"claude-code","kind":"setting","key":"role"}`, "not a kind the client answers"},
@@ -28,8 +28,8 @@ func giteaBoundExpected(internalToken, password string) string {
return "GITEA__security__INTERNAL_TOKEN=" + internalToken + "\n" +
"GITEA__database__DB_TYPE=postgres\n" +
"GITEA__database__HOST=10.7.0.1:5432\n" +
"GITEA__database__NAME=mesh_g14_gitea\n" +
"GITEA__database__USER=mesh_g14_gitea\n" +
"GITEA__database__NAME=mesh_node-b_gitea\n" +
"GITEA__database__USER=mesh_node-b_gitea\n" +
"GITEA__database__PASSWD=" + password + "\n" +
"GITEA__webhook__ALLOWED_HOST_LIST=172.17.0.1\n"
}
@@ -39,15 +39,15 @@ func giteaRun(t *testing.T, entry string, extra string) (*engine, *aBus, *report
b := startBus(t)
registryOf679(t, b)
vault := startVault(t, b)
vault.keep("own/g14/gitea/internal-token", "tok-never-on-the-bus", false)
vault.keep("pair/novox/postgres-database/g14/gitea", "pw-never-on-the-bus", false)
vault.keep("own/node-b/gitea/internal-token", "tok-never-on-the-bus", false)
vault.keep("pair/node-c/postgres-database/node-b/gitea", "pw-never-on-the-bus", false)
heard := b.listen(t)
e := startEngine(t, map[string]string{giteaEnvPath: "owned"})
tpl, _ := templates(t, map[string]string{"server.env": giteaTemplate(t) + extra})
b.assign(t, "gitea", Plan{Templates: tpl, Files: []PlannedFile{{ID: "env", Path: giteaEnvPath,
Template: "server.env", OwnerAtRuntime: "owned"}},
Secrets: []PlannedSecret{{Name: "internal-token"}},
Provisions: []PlannedProvision{{Name: "postgres-database", Provision: "postgres-database", Provider: "novox",
Provisions: []PlannedProvision{{Name: "postgres-database", Provision: "postgres-database", Provider: "node-c",
ProviderModule: "postgres", CredentialForm: "pair"}}})
kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "postgres_bound"})
if err != nil {
@@ -60,19 +60,19 @@ func giteaRun(t *testing.T, entry string, extra string) (*engine, *aBus, *report
}
func TestNatsAHandedGiteasServerEnvIsFilledAsTheControllerComposedIt(t *testing.T) {
e, _, _ := giteaRun(t, `{"provider":"novox","provider-module":"postgres","provision":"postgres-database",`+
`"consumer":"g14","module":"gitea","as":"mesh_g14_gitea","user":"mesh_g14_gitea",`+
`"values":{"at":"10.7.0.1","from":"novox","port":5432},"vault-name":"pair/novox/postgres-database/g14/gitea"}`,
e, _, _ := giteaRun(t, `{"provider":"node-c","provider-module":"postgres","provision":"postgres-database",`+
`"consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea","user":"mesh_node-b_gitea",`+
`"values":{"at":"10.7.0.1","from":"node-c","port":5432},"vault-name":"pair/node-c/postgres-database/node-b/gitea"}`,
"FROM=${bound:postgres-database:from}\n")
eventually(t, "server.env is filled to the controller's text", func() bool {
return e.read(t, giteaEnvPath) == giteaBoundExpected("tok-never-on-the-bus", "pw-never-on-the-bus")+"FROM=novox\n"
return e.read(t, giteaEnvPath) == giteaBoundExpected("tok-never-on-the-bus", "pw-never-on-the-bus")+"FROM=node-c\n"
})
}
// An entry without `at`, or without `from`, fills nothing: the module fails naming the key, never a file with a hole.
func TestNatsAnEntryWithoutAtOrFromLeavesGiteasServerEnvUnfilled(t *testing.T) {
for key, values := range map[string]string{"at": `{"from":"novox","port":5432}`, "from": `{"at":"10.7.0.1","port":5432}`} {
e, _, heard := giteaRun(t, `{"as":"mesh_g14_gitea","user":"mesh_g14_gitea","values":`+values+`}`,
for key, values := range map[string]string{"at": `{"from":"node-c","port":5432}`, "from": `{"at":"10.7.0.1","port":5432}`} {
e, _, heard := giteaRun(t, `{"as":"mesh_node-b_gitea","user":"mesh_node-b_gitea","values":`+values+`}`,
"FROM=${bound:postgres-database:from}\n")
eventually(t, "the module fails for the missing "+key, func() bool {
r, _ := heard.last("gitea")
@@ -99,24 +99,24 @@ func TestNatsAHandedGiteasOriginsAreFilledFromTheRouteProvidersEntry(t *testing.
tpl, _ := templates(t, map[string]string{"origins": string(raw)})
b.assign(t, "gitea", Plan{Templates: tpl, Files: []PlannedFile{{ID: "origins", Path: originsPath,
Template: "origins", OwnerAtRuntime: "owned"}},
Provisions: []PlannedProvision{{Provision: "route", Provider: "novox", ProviderModule: "route-adapter"}}})
Provisions: []PlannedProvision{{Provision: "route", Provider: "node-c", ProviderModule: "route-adapter"}}})
kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "route-adapter_bound"})
if err != nil {
t.Fatal(err)
}
b.kv["route-adapter_bound"] = kv
b.put(t, "route-adapter_bound", node+".gitea.route", `{"provider":"novox","provider-module":"route-adapter",`+
`"provision":"route","consumer":"g14","module":"gitea","as":"mesh_g14_gitea",`+
`"values":{"name":"git.example.org","internal-name":"git.novox.internal","name-web":"git.example.org"}}`)
b.put(t, "route-adapter_bound", node+".gitea.route", `{"provider":"node-c","provider-module":"route-adapter",`+
`"provision":"route","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea",`+
`"values":{"name":"git.example.org","internal-name":"git.node-c.internal","name-web":"git.example.org"}}`)
aClient(t, b, e, t.TempDir())
eventually(t, "origins is filled to the controller's text", func() bool {
return e.read(t, originsPath) == "https://git.example.org\n"
})
}
// The same where route-proxy, not route-adapter, answers the route, as on novox (issue 792): filled by its machine's client from the route provider's entry, which
// The same where route-proxy, not route-adapter, answers the route, as on node-c (issue 792): filled by its machine's client from the route provider's entry, which
// carries the public name the controller composed for gitea, to the text the controller wrote for the binding file.
func TestNatsAHandedGiteasOriginsAreFilledFromRouteProxysEntryOnNovox(t *testing.T) {
func TestNatsAHandedGiteasOriginsAreFilledFromRouteProxysEntryOnNodeC(t *testing.T) {
const originsPath = "/etc/gitea/origins"
raw, err := os.ReadFile("../../../gitea/files/origins")
if err != nil {
@@ -129,15 +129,15 @@ func TestNatsAHandedGiteasOriginsAreFilledFromRouteProxysEntryOnNovox(t *testing
tpl, _ := templates(t, map[string]string{"origins": string(raw)})
b.assign(t, "gitea", Plan{Templates: tpl, Files: []PlannedFile{{ID: "origins", Path: originsPath,
Template: "origins", OwnerAtRuntime: "owned"}},
Provisions: []PlannedProvision{{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"}}})
Provisions: []PlannedProvision{{Provision: "route", Provider: "node-c", ProviderModule: "route-proxy"}}})
kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "route-proxy_bound"})
if err != nil {
t.Fatal(err)
}
b.kv["route-proxy_bound"] = kv
b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"novox","provider-module":"route-proxy",`+
`"provision":"route","consumer":"g14","module":"gitea","as":"mesh_g14_gitea",`+
`"values":{"name":"git.example.org","internal-name":"git.novox.internal","name-web":"git.example.org"}}`)
b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"node-c","provider-module":"route-proxy",`+
`"provision":"route","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea",`+
`"values":{"name":"git.example.org","internal-name":"git.node-c.internal","name-web":"git.example.org"}}`)
aClient(t, b, e, t.TempDir())
eventually(t, "origins is filled to the controller's text", func() bool {
return e.read(t, originsPath) == "https://git.example.org\n"
@@ -35,7 +35,7 @@ import (
// node-engine's own tests prove its writes; these prove the client asks for the right ones — and counting each
// write that moved a file, which is what the node-engine reloads or restarts the program for.
const node = "g14"
const node = "node-b"
// aBus is a server of this test alone with the client's buckets, the registry written as mesh-provisioner writes it.
type aBus struct {
@@ -343,7 +343,7 @@ func templates(t *testing.T, files map[string]string) (Templates, *httptest.Serv
return Templates{Source: srv.URL + "/files.tar.gz", Digest: digestOf(body)}, srv
}
// aClient runs a client on g14's account until the returned stop is called.
// aClient runs a client on node-b's account until the returned stop is called.
func aClient(t *testing.T, b *aBus, e *engine, state string) (*Client, func()) {
t.Helper()
nc, err := nats.Connect(b.url, nats.UserInfo("provisioner."+node, "x"),
@@ -432,7 +432,7 @@ func TestNatsATemplateFillsFromEachOwnersKeys(t *testing.T) {
// The needs, one per placeholder, under the node's and the module's name.
eventually(t, "the needs are recorded", func() bool {
got := b.keys(t, NeedsBucket, node+".dunst.")
return len(got) == 2 && got[0] == "g14.dunst.config.font-size" && got[1] == "g14.dunst.machine.account-home"
return len(got) == 2 && got[0] == "node-b.dunst.config.font-size" && got[1] == "node-b.dunst.machine.account-home"
})
kept, err := b.kv[AppliedBucket].Get(context.Background(), node+".dunst")
if err != nil || !strings.Contains(string(kept.Value()), `"status":"applied"`) {
@@ -573,8 +573,8 @@ func TestNatsWithNothingOverTheBusItAsksForNoWriteNoSecretAndNoReport(t *testing
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep("own/g14/app/app-token", "tok", false)
vault.keep("own/g14/app/db-password", "pw", false)
vault.keep("own/node-b/app/app-token", "tok", false)
vault.keep("own/node-b/app/db-password", "pw", false)
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
b.config(t, "app", "font-size", "13")
b.assign(t, "app", app(t))
@@ -585,7 +585,7 @@ func TestNatsWithNothingOverTheBusItAsksForNoWriteNoSecretAndNoReport(t *testing
return r.Status == StatusApplied
})
_, askedBefore := e.counts(appConf)
vaultBefore := vault.times("own/g14/app/app-token") + vault.times("own/g14/app/db-password")
vaultBefore := vault.times("own/node-b/app/app-token") + vault.times("own/node-b/app/db-password")
_, reportsBefore := heard.last("app")
// Well over any former drift interval used in these tests (100ms, 200ms): nothing reaches the client.
@@ -594,7 +594,7 @@ func TestNatsWithNothingOverTheBusItAsksForNoWriteNoSecretAndNoReport(t *testing
if _, askedAfter := e.counts(appConf); askedAfter != askedBefore {
t.Errorf("the node-engine was asked to write %s again, with nothing over the bus", appConf)
}
if n := vault.times("own/g14/app/app-token") + vault.times("own/g14/app/db-password"); n != vaultBefore {
if n := vault.times("own/node-b/app/app-token") + vault.times("own/node-b/app/db-password"); n != vaultBefore {
t.Error("the vault was asked again, with nothing over the bus")
}
if _, after := heard.last("app"); after != reportsBefore {
@@ -661,7 +661,7 @@ func TestNatsWhatCannotBeFilledFailsWithTheReason(t *testing.T) {
e := startEngine(t, map[string]string{dunstrc: "owned", "/etc/app.conf": "owned"})
aClient(t, b, e, t.TempDir())
b.put(t, AssignmentsBucket, node+".broken", `{"node":"g14","module":"broken","provisioning":{"templates":`+
b.put(t, AssignmentsBucket, node+".broken", `{"node":"node-b","module":"broken","provisioning":{"templates":`+
`{"source":"x","digest":"sha256:00"},"files":[],"surprise":1}}`)
p := dunst(t)
p.Templates.Digest = "sha256:" + strings.Repeat("0", 64)
@@ -684,10 +684,10 @@ func TestNatsWhatCannotBeFilledFailsWithTheReason(t *testing.T) {
func TestANeedsKeyHoldsAnyName(t *testing.T) {
for n, want := range map[Need]string{
{Kind: "config", Name: "font-size"}: "g14.dunst.config.font-size",
{Kind: "seat", Name: "mesh-broker:4222"}: "g14.dunst.seat.mesh-broker=3A4222",
{Kind: "config", Name: "a=b c"}: "g14.dunst.config.a=3Db=20c",
{Kind: "machine", Name: "account-home"}: "g14.dunst.machine.account-home",
{Kind: "config", Name: "font-size"}: "node-b.dunst.config.font-size",
{Kind: "seat", Name: "mesh-broker:4222"}: "node-b.dunst.seat.mesh-broker=3A4222",
{Kind: "config", Name: "a=b c"}: "node-b.dunst.config.a=3Db=20c",
{Kind: "machine", Name: "account-home"}: "node-b.dunst.machine.account-home",
} {
if got := NeedKey(node, "dunst", n); got != want {
t.Errorf("%v: %s, want %s", n, got, want)
@@ -51,8 +51,8 @@ func TestNatsASelectingPlaceholderTakesOnlyTheItemsCarryingItsAttributesInOrder(
eventually(t, "the file holds each placeholder's items", func() bool { return e.read(t, barsConf) == want })
eventually(t, "the named items are recorded under their module and name", func() bool {
got := b.keys(t, CollectBucket, node+".blocks.")
return len(got) == 4 && got[0] == "g14.blocks.another" && got[1] == "g14.blocks.power/battery" &&
got[2] == "g14.blocks.power/clock" && got[3] == "g14.blocks.power/cpu"
return len(got) == 4 && got[0] == "node-b.blocks.another" && got[1] == "node-b.blocks.power/battery" &&
got[2] == "node-b.blocks.power/clock" && got[3] == "node-b.blocks.power/cpu"
})
// An item no longer contributed goes, and only that one.
@@ -91,13 +91,13 @@ func TestASelectorIsReadFromAPlaceholderOfAnyNamespace(t *testing.T) {
func TestAnItemsKeyHoldsItsName(t *testing.T) {
key := CollectKey(node, "blocks", itemID("power", "battery"))
if key != "g14.blocks.power/battery" {
if key != "node-b.blocks.power/battery" {
t.Fatal(key)
}
if c, id, ok := itemOfKey(node, key); !ok || c != "blocks" || id != "power/battery" {
t.Errorf("reads back as %q %q %v", c, id, ok)
}
for _, bad := range []string{"g14.blocks.power/", "g14.blocks./x", "g14.blocks.a/b/c"} {
for _, bad := range []string{"node-b.blocks.power/", "node-b.blocks./x", "node-b.blocks.a/b/c"} {
if _, _, ok := itemOfKey(node, bad); ok {
t.Errorf("%s read as an item's key", bad)
}
@@ -115,15 +115,15 @@ func TestAnItemsKeyHoldsItsName(t *testing.T) {
func TestAnItemOfAPlanReadsItsNameAndAttributesAndRefusesAnUnknownField(t *testing.T) {
plan := func(item string) string {
return `{"node":"g14","module":"power","provisioning":{"templates":{"source":"x","digest":"sha256:` +
return `{"node":"node-b","module":"power","provisioning":{"templates":{"source":"x","digest":"sha256:` +
strings.Repeat("0", 64) + `"},"files":[],"items":[` + item + `]}}`
}
_, p, err := planOf("g14", "g14.power", []byte(plan(
_, p, err := planOf("node-b", "node-b.power", []byte(plan(
`{"collection":"blocks","name":"battery","attributes":{"bar":"top"},"template":"battery"}`)))
if err != nil || len(p.Items) != 1 || p.Items[0].Name != "battery" || p.Items[0].Attributes["bar"] != "top" {
t.Fatalf("read %+v (%v)", p.Items, err)
}
if _, _, err := planOf("g14", "g14.power", []byte(plan(
if _, _, err := planOf("node-b", "node-b.power", []byte(plan(
`{"collection":"blocks","template":"battery","colour":"red"}`))); err == nil || !strings.Contains(err.Error(), "colour") {
t.Errorf("an unknown item field was accepted: %v", err)
}
@@ -47,10 +47,10 @@ func TestNatsZshsFileHoldsEveryPlacedContributorsItemAndLosesOneWhoseContributor
// Each item recorded under the node, the collection and its contributor; filled, the contributor's report says so.
eventually(t, "the items are recorded", func() bool {
got := b.keys(t, CollectBucket, node+".")
return len(got) == 3 && got[0] == "g14.shell=3Azsh=3Anormal.mesh-cli" &&
got[1] == "g14.shell=3Azsh=3Anormal.powerlevel10k" && got[2] == "g14.zsh-widgets.fzf"
return len(got) == 3 && got[0] == "node-b.shell=3Azsh=3Anormal.mesh-cli" &&
got[1] == "node-b.shell=3Azsh=3Anormal.powerlevel10k" && got[2] == "node-b.zsh-widgets.fzf"
})
kept, err := b.kv[CollectBucket].Get(context.Background(), "g14.shell=3Azsh=3Anormal.powerlevel10k")
kept, err := b.kv[CollectBucket].Get(context.Background(), "node-b.shell=3Azsh=3Anormal.powerlevel10k")
if err != nil {
t.Fatal(err)
}
@@ -66,7 +66,7 @@ func TestNatsZshsFileHoldsEveryPlacedContributorsItemAndLosesOneWhoseContributor
// zsh records the collections it reads as its needs, like any value.
eventually(t, "zsh's needs are recorded", func() bool {
got := b.keys(t, NeedsBucket, node+".zsh.")
return len(got) == 2 && got[0] == "g14.zsh.collected.zsh-widgets" && got[1] == "g14.zsh.shell.zsh=3Anormal"
return len(got) == 2 && got[0] == "node-b.zsh.collected.zsh-widgets" && got[1] == "node-b.zsh.shell.zsh=3Anormal"
})
// A contributor's own value changes: its item, and so zshrc, follow.
@@ -136,9 +136,9 @@ func TestNatsACollectionNotPlannedAndAnItemNamingASecretFail(t *testing.T) {
func TestAnItemsKeyHoldsAnyCollectionAndReadsBack(t *testing.T) {
for collection, want := range map[string]string{
"zsh-widgets": "g14.zsh-widgets.fzf",
"shell:zsh:normal": "g14.shell=3Azsh=3Anormal.fzf",
"contribution:node-hotkeys:trigger": "g14.contribution=3Anode-hotkeys=3Atrigger.fzf",
"zsh-widgets": "node-b.zsh-widgets.fzf",
"shell:zsh:normal": "node-b.shell=3Azsh=3Anormal.fzf",
"contribution:node-hotkeys:trigger": "node-b.contribution=3Anode-hotkeys=3Atrigger.fzf",
} {
key := CollectKey(node, collection, "fzf")
if key != want {
@@ -149,7 +149,7 @@ func TestAnItemsKeyHoldsAnyCollectionAndReadsBack(t *testing.T) {
t.Errorf("%s reads back as %q %q %v", key, c, who, ok)
}
}
for _, bad := range []string{"g14.x", "ace.zsh-widgets.fzf", "g14.=ZZ.fzf", "g14.a.b.c", "g14.Upper.fzf"} {
for _, bad := range []string{"node-b.x", "node-a.zsh-widgets.fzf", "node-b.=ZZ.fzf", "node-b.a.b.c", "node-b.Upper.fzf"} {
if _, _, ok := itemOfKey(node, bad); ok {
t.Errorf("%s read as an item's key", bad)
}
@@ -93,25 +93,25 @@ func TestNatsAProvidersConsumersAreFilledFromTheVaultAndFollowEachBindAndRemake(
heard := b.listen(t)
wire := sniff(t, b)
vault := startVault(t, b)
vault.keep(pairOf("ace", "letta", ""), "letta-pw-never-on-the-bus", false)
vault.keep(pairOf("shanks", "gitea", "cache"), "gitea-pw-never-on-the-bus", false)
// Bound to this provider: ace's letta, and shanks's gitea under a local name. Not: a provision of another
vault.keep(pairOf("node-a", "letta", ""), "letta-pw-never-on-the-bus", false)
vault.keep(pairOf("node-d", "gitea", "cache"), "gitea-pw-never-on-the-bus", false)
// Bound to this provider: node-a's letta, and node-d's gitea under a local name. Not: a provision of another
// provider, another provision, and a credential its provider's software issued per holder (issue 757).
b.bind(t, "ace", "letta", fromHere(""),
PlannedProvision{Provision: "postgres-database", Local: "old", Provider: "ace", ProviderModule: "postgres"})
b.bind(t, "shanks", "gitea", PlannedProvision{Provision: "redis", Provider: node, ProviderModule: "postgres"},
b.bind(t, "node-a", "letta", fromHere(""),
PlannedProvision{Provision: "postgres-database", Local: "old", Provider: "node-a", ProviderModule: "postgres"})
b.bind(t, "node-d", "gitea", PlannedProvision{Provision: "redis", Provider: node, ProviderModule: "postgres"},
fromHere("cache"),
PlannedProvision{Name: "token", Provision: "forge-read", Provider: node, ProviderModule: "postgres"})
// Nor a bind whose offer names another credential form than a login per consumer (issue 768).
b.bind(t, "ace", "shared", PlannedProvision{Provision: "postgres-database", Provider: node,
b.bind(t, "node-a", "shared", PlannedProvision{Provision: "postgres-database", Provider: node,
ProviderModule: "postgres", CredentialForm: "shared"})
e := startEngine(t, map[string]string{consumersFile: "owned"})
b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}"))
aClient(t, b, e, t.TempDir())
letta := listed{Consumer: "ace", Module: "letta", Credential: pairOf("ace", "letta", ""),
letta := listed{Consumer: "node-a", Module: "letta", Credential: pairOf("node-a", "letta", ""),
Value: "letta-pw-never-on-the-bus"}
gitea := listed{Consumer: "shanks", Module: "gitea", Local: "cache", Credential: pairOf("shanks", "gitea", "cache"),
gitea := listed{Consumer: "node-d", Module: "gitea", Local: "cache", Credential: pairOf("node-d", "gitea", "cache"),
Value: "gitea-pw-never-on-the-bus"}
eventually(t, "the file lists each bound consumer with its credential, in the plans' order", func() bool {
return sameList(consumersIn(t, e), letta, gitea)
@@ -134,21 +134,21 @@ func TestNatsAProvidersConsumersAreFilledFromTheVaultAndFollowEachBindAndRemake(
eventually(t, "a consumer that binds is listed when the vault holds its credential, in its assignment's order",
func() bool { return sameList(consumersIn(t, e), letta, photos, gitea) })
// The vault remakes a credential.
vault.keep(pairOf("ace", "letta", ""), "letta-pw-remade-never-on-the-bus", true)
vault.keep(pairOf("node-a", "letta", ""), "letta-pw-remade-never-on-the-bus", true)
letta.Value = "letta-pw-remade-never-on-the-bus"
eventually(t, "a credential the vault remakes reaches the file", func() bool {
return sameList(consumersIn(t, e), letta, photos, gitea)
})
// A consumer unbinds.
b.del(t, AssignmentsBucket, "shanks.gitea")
b.del(t, AssignmentsBucket, "node-d.gitea")
eventually(t, "a consumer that unbinds leaves the file", func() bool {
return sameList(consumersIn(t, e), letta, photos)
})
// Another provider's consumer's credential changing is no reason to ask.
before := vault.times(pairOf("ace", "letta", ""))
vault.keep("pair/ace/postgres-database/ace/letta/old", "elsewhere", true)
before := vault.times(pairOf("node-a", "letta", ""))
vault.keep("pair/node-a/postgres-database/node-a/letta/old", "elsewhere", true)
time.Sleep(300 * time.Millisecond)
if vault.times("pair/ace/postgres-database/ace/letta/old") != 0 || vault.times(pairOf("ace", "letta", "")) != before {
if vault.times("pair/node-a/postgres-database/node-a/letta/old") != 0 || vault.times(pairOf("node-a", "letta", "")) != before {
t.Error("a change of another provider's consumer made the client ask")
}
@@ -194,7 +194,7 @@ func appPlan(t *testing.T) Plan {
"PORT=${bound:postgres-database:port}\nUSER=${bound:postgres-database:user}\n"})
return Plan{Templates: tpl, Files: []PlannedFile{{ID: "db", Path: appBound, Template: "database.env",
OwnerAtRuntime: "owned"}},
Provisions: []PlannedProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}}}
Provisions: []PlannedProvision{{Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres"}}}
}
func TestNatsAConsumersBoundValuesAreFilledFromItsProvidersEntry(t *testing.T) {
@@ -216,24 +216,24 @@ func TestNatsAConsumersBoundValuesAreFilledFromItsProvidersEntry(t *testing.T) {
}
b.kv["postgres_bound"] = kv
// The provider has not answered it yet: waited for.
b.put(t, "postgres_bound", "ace.app.postgres-database", `{"values":{"host":"elsewhere"}}`)
b.put(t, "postgres_bound", "node-a.app.postgres-database", `{"values":{"host":"elsewhere"}}`)
b.assign(t, "app", appPlan(t)) // a change of its own is the only reason it looks again: there is no timer
eventually(t, "the consumer waits for its own entry", func() bool {
r, _ := heard.last("app")
return r.Status == StatusWaiting && strings.Contains(strings.Join(r.WaitsFor, " "), "postgres on novox")
return r.Status == StatusWaiting && strings.Contains(strings.Join(r.WaitsFor, " "), "postgres on node-c")
})
b.put(t, "postgres_bound", node+".app.postgres-database", `{"provider":"novox","provider-module":"postgres",`+
`"provision":"postgres-database","consumer":"g14","module":"app","as":"mesh_g14_app","user":"mesh_g14_app",`+
`"values":{"host":"novox.mesh","port":5432},"vault-name":"pair/novox/postgres-database/g14/app"}`)
b.put(t, "postgres_bound", node+".app.postgres-database", `{"provider":"node-c","provider-module":"postgres",`+
`"provision":"postgres-database","consumer":"node-b","module":"app","as":"mesh_node-b_app","user":"mesh_node-b_app",`+
`"values":{"host":"node-c.mesh","port":5432},"vault-name":"pair/node-c/postgres-database/node-b/app"}`)
eventually(t, "the file is filled from its provider's entry", func() bool {
return e.read(t, appBound) == "HOST=novox.mesh\nPORT=5432\nUSER=mesh_g14_app\n"
return e.read(t, appBound) == "HOST=node-c.mesh\nPORT=5432\nUSER=mesh_node-b_app\n"
})
b.put(t, "postgres_bound", node+".app.postgres-database", `{"user":"mesh_g14_app",`+
`"values":{"host":"novox.lan","port":5432}}`)
b.put(t, "postgres_bound", node+".app.postgres-database", `{"user":"mesh_node-b_app",`+
`"values":{"host":"node-c.lan","port":5432}}`)
eventually(t, "a changed entry refills the file", func() bool {
return e.read(t, appBound) == "HOST=novox.lan\nPORT=5432\nUSER=mesh_g14_app\n"
return e.read(t, appBound) == "HOST=node-c.lan\nPORT=5432\nUSER=mesh_node-b_app\n"
})
b.put(t, "postgres_bound", node+".app.postgres-database", `{"refused":"app on g14 keeps several holders"}`)
b.put(t, "postgres_bound", node+".app.postgres-database", `{"refused":"app on node-b keeps several holders"}`)
eventually(t, "an entry the provider refused fails the consumer with its reason", func() bool {
r, _ := heard.last("app")
return r.Status == StatusFailed && strings.Contains(r.Reason, "keeps several holders")
@@ -246,17 +246,17 @@ func TestNatsAProviderListsAConsumerWhoseEntryNamesItsLoginFile(t *testing.T) {
b := startBus(t)
registryOf679(t, b)
vault := startVault(t, b)
vault.keep(pairOf("ace", "nextcloud", ""), "nc-pw-never-on-the-bus", false)
vault.keep(pairOf("ace", "forge", ""), "forge-pw", false)
b.bind(t, "ace", "nextcloud", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node,
vault.keep(pairOf("node-a", "nextcloud", ""), "nc-pw-never-on-the-bus", false)
vault.keep(pairOf("node-a", "forge", ""), "forge-pw", false)
b.bind(t, "node-a", "nextcloud", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node,
ProviderModule: "postgres", CredentialForm: "pair", Path: "/s/database.secret"})
b.bind(t, "ace", "forge", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node,
b.bind(t, "node-a", "forge", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node,
ProviderModule: "postgres", CredentialForm: "issued", Path: "/s/forge.secret"})
e := startEngine(t, map[string]string{consumersFile: "owned"})
b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}"))
aClient(t, b, e, t.TempDir())
eventually(t, "the consumer with a login file is listed, the per-holder one is not", func() bool {
return sameList(consumersIn(t, e), listed{Consumer: "ace", Module: "nextcloud",
Credential: pairOf("ace", "nextcloud", ""), Value: "nc-pw-never-on-the-bus"})
return sameList(consumersIn(t, e), listed{Consumer: "node-a", Module: "nextcloud",
Credential: pairOf("node-a", "nextcloud", ""), Value: "nc-pw-never-on-the-bus"})
})
}
@@ -48,13 +48,13 @@ func TestNatsAMovedGiteaAndClaudeCodeHaveTheirFilesFilledFromTheBus(t *testing.T
origins = "/etc/gitea/origins"
facts = "/var/lib/claude-code/state/facts.json"
readCred = "/var/lib/claude-code/state/gitea-read.secret"
credential = "https://agent-g14:gta-never-on-the-bus@git.example.org"
credential = "https://agent-node-b:gta-never-on-the-bus@git.example.org"
)
b := startBus(t)
registryOf679(t, b)
vault := startVault(t, b)
vault.keep("own/g14/gitea/internal-token", "tok-never-on-the-bus", false)
vault.keep("pair/novox/postgres-database/g14/gitea", "pw-never-on-the-bus", false)
vault.keep("own/node-b/gitea/internal-token", "tok-never-on-the-bus", false)
vault.keep("pair/node-c/postgres-database/node-b/gitea", "pw-never-on-the-bus", false)
vault.keep(forgeHolder, credential, false)
e := startEngine(t, map[string]string{serverEnv: "owned", origins: "owned", facts: "owned", readCred: "owned"})
@@ -65,16 +65,16 @@ func TestNatsAMovedGiteaAndClaudeCodeHaveTheirFilesFilledFromTheBus(t *testing.T
{ID: "origins", Path: origins, Template: "origins", OwnerAtRuntime: "owned"}},
Secrets: []PlannedSecret{{Name: "internal-token"}},
Provisions: []PlannedProvision{
{Name: "postgres-database", Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres",
{Name: "postgres-database", Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres",
CredentialForm: "pair"},
{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"}}})
{Provision: "route", Provider: "node-c", ProviderModule: "route-proxy"}}})
ccTpl, _ := templates(t, map[string]string{"facts.json": catalogFile(t, "claude-code/files/facts.json")})
b.assign(t, "claude-code", Plan{Templates: ccTpl, Files: []PlannedFile{
{ID: "facts", Path: facts, Template: "facts.json", OwnerAtRuntime: "owned"}},
Placement: map[string]string{"machine:name": node},
Provisions: []PlannedProvision{
{Provision: "mcp-endpoint", Provider: node, ProviderModule: "node-tools"},
{Name: "gitea-read", Provision: "gitea-read", Provider: "novox", Path: readCred}}})
{Name: "gitea-read", Provision: "gitea-read", Provider: "node-c", Path: readCred}}})
for _, bucket := range []string{"postgres_bound", "route-proxy_bound", "node-tools_bound"} {
kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: bucket})
@@ -83,21 +83,21 @@ func TestNatsAMovedGiteaAndClaudeCodeHaveTheirFilesFilledFromTheBus(t *testing.T
}
b.kv[bucket] = kv
}
b.put(t, "postgres_bound", node+".gitea.postgres-database", `{"provider":"novox","provider-module":"postgres",`+
`"provision":"postgres-database","consumer":"g14","module":"gitea","as":"mesh_g14_gitea","user":"mesh_g14_gitea",`+
`"values":{"at":"10.7.0.1","from":"novox","port":5432},"vault-name":"pair/novox/postgres-database/g14/gitea"}`)
b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"novox","provider-module":"route-proxy",`+
`"provision":"route","consumer":"g14","module":"gitea","as":"mesh_g14_gitea",`+
`"values":{"name":"git.example.org","internal-name":"git.novox.internal","name-web":"git.example.org"}}`)
b.put(t, "node-tools_bound", node+".claude-code.mcp-endpoint", `{"provider":"g14","provider-module":"node-tools",`+
`"provision":"mcp-endpoint","consumer":"g14","module":"claude-code","as":"mesh_g14_claude-code",`+
b.put(t, "postgres_bound", node+".gitea.postgres-database", `{"provider":"node-c","provider-module":"postgres",`+
`"provision":"postgres-database","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea","user":"mesh_node-b_gitea",`+
`"values":{"at":"10.7.0.1","from":"node-c","port":5432},"vault-name":"pair/node-c/postgres-database/node-b/gitea"}`)
b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"node-c","provider-module":"route-proxy",`+
`"provision":"route","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea",`+
`"values":{"name":"git.example.org","internal-name":"git.node-c.internal","name-web":"git.example.org"}}`)
b.put(t, "node-tools_bound", node+".claude-code.mcp-endpoint", `{"provider":"node-b","provider-module":"node-tools",`+
`"provision":"mcp-endpoint","consumer":"node-b","module":"claude-code","as":"mesh_node-b_claude-code",`+
`"values":{"port":4270}}`)
aClient(t, b, e, t.TempDir())
for path, want := range map[string]string{
serverEnv: giteaBoundExpected("tok-never-on-the-bus", "pw-never-on-the-bus"),
origins: "https://git.example.org\n",
facts: "{\n \"node\": \"g14\",\n \"console\": \"http://127.0.0.1:4270/mcp\"\n}\n",
facts: "{\n \"node\": \"node-b\",\n \"console\": \"http://127.0.0.1:4270/mcp\"\n}\n",
readCred: credential,
} {
eventually(t, path+" is filled to the controller's text", func() bool { return e.read(t, path) == want })
@@ -10,20 +10,20 @@ import (
// client reads that strictly, before the controller writes it, and fills nothing from an entry with no credential;
// the vault acts on it.
func TestAPlanNamingItsProvidersIsRead(t *testing.T) {
value := `{"node":"g14","module":"app","provisioning":{` +
value := `{"node":"node-b","module":"app","provisioning":{` +
`"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},` +
`"files":[{"id":"conf","path":"/etc/app.conf","template":"app.conf","owner-at-runtime":"owned"}],` +
`"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres"},` +
`{"provision":"s3-bucket","local":"hot","provider":"ace","provider-module":"minio"},` +
`{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` +
`"provisions":[{"provision":"postgres-database","provider":"node-c","provider-module":"postgres"},` +
`{"provision":"s3-bucket","local":"hot","provider":"node-a","provider-module":"minio"},` +
`{"name":"forge-read","provision":"forge-read","provider":"node-c","provider-module":"forge",` +
`"path":"/var/lib/app/forge.secret"}]}}`
_, p, err := planOf("g14", "g14.app", []byte(value))
_, p, err := planOf("node-b", "node-b.app", []byte(value))
if err != nil {
t.Fatal(err)
}
want := []PlannedProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"},
{Provision: "s3-bucket", Local: "hot", Provider: "ace", ProviderModule: "minio"},
{Name: "forge-read", Provision: "forge-read", Provider: "novox", ProviderModule: "forge",
want := []PlannedProvision{{Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres"},
{Provision: "s3-bucket", Local: "hot", Provider: "node-a", ProviderModule: "minio"},
{Name: "forge-read", Provision: "forge-read", Provider: "node-c", ProviderModule: "forge",
Path: "/var/lib/app/forge.secret"}}
if len(p.Provisions) != len(want) {
t.Fatalf("the plan's provisions are read as %+v, want %+v", p.Provisions, want)
@@ -55,16 +55,16 @@ func TestAPlanNamingItsProvidersIsRead(t *testing.T) {
// Each entry names the credential form of its provider's offer (issue 768), which the vault acts on: the client
// reads it strictly, before the controller writes it, and refuses a form it does not know.
func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) {
value := `{"node":"g14","module":"app","provisioning":{` +
value := `{"node":"node-b","module":"app","provisioning":{` +
`"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},` +
`"files":[{"id":"conf","path":"/etc/app.conf","template":"app.conf","owner-at-runtime":"owned"}],` +
`"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` +
`"provisions":[{"provision":"postgres-database","provider":"node-c","provider-module":"postgres",` +
`"credential-form":"pair"},` +
`{"provision":"sonarr-api","provider":"ace","provider-module":"sonarr","credential-form":"shared"},` +
`{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` +
`{"provision":"sonarr-api","provider":"node-a","provider-module":"sonarr","credential-form":"shared"},` +
`{"name":"forge-read","provision":"forge-read","provider":"node-c","provider-module":"forge",` +
`"credential-form":"issued","path":"/var/lib/app/forge.secret"},` +
`{"provision":"smtp-relay","provider":"ace","provider-module":"relay","credential-form":"given"}]}}`
_, p, err := planOf("g14", "g14.app", []byte(value))
`{"provision":"smtp-relay","provider":"node-a","provider-module":"relay","credential-form":"given"}]}}`
_, p, err := planOf("node-b", "node-b.app", []byte(value))
if err != nil {
t.Fatal(err)
}
@@ -76,7 +76,7 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) {
t.Fatalf("the credential forms are read as %v", forms)
}
bad := strings.Replace(value, `"credential-form":"pair"`, `"credential-form":"one-for-all"`, 1)
if _, _, err := planOf("g14", "g14.app", []byte(bad)); err == nil || !strings.Contains(err.Error(), "one-for-all") {
if _, _, err := planOf("node-b", "node-b.app", []byte(bad)); err == nil || !strings.Contains(err.Error(), "one-for-all") {
t.Fatalf("a credential form the client does not know was taken: %v", err)
}
}
@@ -84,12 +84,12 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) {
// A consumer's plan entry may carry what its module contributes to the provider (issue 776): the client reads it
// strictly, keeps it on the entry, and still refuses any other field it does not know.
func TestAPlanEntryCarryingItsContributionIsRead(t *testing.T) {
value := `{"node":"g14","module":"app","provisioning":{` +
value := `{"node":"node-b","module":"app","provisioning":{` +
`"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},` +
`"files":[{"id":"conf","path":"/etc/app.conf","template":"app.conf","owner-at-runtime":"owned"}],` +
`"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` +
`"provisions":[{"provision":"postgres-database","provider":"node-c","provider-module":"postgres",` +
`"contributes":{"name":"nextcloud","extensions":["pg_trgm"]}}]}}`
_, p, err := planOf("g14", "g14.app", []byte(value))
_, p, err := planOf("node-b", "node-b.app", []byte(value))
if err != nil {
t.Fatal(err)
}
@@ -98,7 +98,7 @@ func TestAPlanEntryCarryingItsContributionIsRead(t *testing.T) {
t.Fatalf("the plan's provisions are read as %+v, want the contribution %v kept", p.Provisions, want)
}
bad := strings.Replace(value, `"contributes":`, `"contribution":`, 1)
if _, _, err := planOf("g14", "g14.app", []byte(bad)); err == nil {
if _, _, err := planOf("node-b", "node-b.app", []byte(bad)); err == nil {
t.Error("a plan entry with a field the client does not know was taken")
}
}
@@ -28,14 +28,14 @@ func TestAPulledInModulesValueIsReadForItsPlan(t *testing.T) {
if err != nil {
t.Fatal(err)
}
value := fmt.Sprintf(`{"node":"g14","module":"mesh-cli","required-by":"desk","provisioning":%s}`, p)
module, got, err := planOf("g14", "g14.mesh-cli", []byte(value))
value := fmt.Sprintf(`{"node":"node-b","module":"mesh-cli","required-by":"desk","provisioning":%s}`, p)
module, got, err := planOf("node-b", "node-b.mesh-cli", []byte(value))
if err != nil || module != "mesh-cli" || len(got.Items) != 1 || got.Items[0].Collection != "shell:zsh:normal" {
t.Fatalf("a pulled-in module's value reads as %s %+v (%v)", module, got, err)
}
// Still strict in the plan.
spoilt := strings.Replace(value, `"items":`, `"surprise":1,"items":`, 1)
if _, _, err := planOf("g14", "g14.mesh-cli", []byte(spoilt)); err == nil {
if _, _, err := planOf("node-b", "node-b.mesh-cli", []byte(spoilt)); err == nil {
t.Fatalf("a pulled-in module's plan with an unknown field is acted on: %s", spoilt)
}
}
@@ -45,8 +45,8 @@ func TestNatsAPulledInModuleIsFilledWithItsSecretAndItsItemIsRecorded(t *testing
heard := b.listen(t)
vault := startVault(t, b)
const token, password = "tok-765-never-on-the-bus", "pw-765-never-on-the-bus"
vault.keep("own/g14/app/app-token", token, false)
vault.keep("own/g14/app/db-password", password, false)
vault.keep("own/node-b/app/app-token", token, false)
vault.keep("own/node-b/app/db-password", password, false)
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned", zshrc: "owned"})
b.config(t, "app", "font-size", "13")
b.pullIn(t, "app", "desk", app(t))
@@ -60,7 +60,7 @@ func TestNatsAPulledInModuleIsFilledWithItsSecretAndItsItemIsRecorded(t *testing
eventually(t, "its secret's own file holds it alone", func() bool { return e.read(t, appDB) == password })
eventually(t, "the pulled-in contributor's item is recorded", func() bool {
got := b.keys(t, CollectBucket, node+".")
return len(got) == 1 && got[0] == "g14.shell=3Azsh=3Anormal.mesh-cli"
return len(got) == 1 && got[0] == "node-b.shell=3Azsh=3Anormal.mesh-cli"
})
eventually(t, "zshrc holds the pulled-in contributor's item", func() bool {
return e.read(t, zshrc) == "# zsh\n# mesh-cli\nalias nox=mesh-cli\n# then\n# end\n"
@@ -55,7 +55,7 @@ func routeEntryFor(t *testing.T, module, planEntry string) string {
bus := &memBus{buckets: map[string]map[string][]byte{provider.AssignmentsBucket: {
node + "." + module: []byte(`{"node":"` + node + `","module":"` + module + `","provisioning":{"provisions":[` +
planEntry + `]}}`)}}}
p := provider.New(provider.Options{Node: "novox", Module: "route-proxy", Bus: bus, Logf: t.Logf,
p := provider.New(provider.Options{Node: "node-c", Module: "route-proxy", Bus: bus, Logf: t.Logf,
Offers: map[string]provider.Offer{"route": {Credential: provider.NoCredential, Serves: map[string]any{}}}})
out, err := p.Pass(context.Background())
if err != nil {
@@ -81,7 +81,7 @@ func routeRun(t *testing.T, module, id, path, templatePath, entryKey, firstEntry
tpl, _ := templates(t, map[string]string{id: string(raw)})
b.assign(t, module, Plan{Templates: tpl, Files: []PlannedFile{{ID: id, Path: path, Template: id,
OwnerAtRuntime: "owned"}},
Provisions: []PlannedProvision{{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"}}})
Provisions: []PlannedProvision{{Provision: "route", Provider: "node-c", ProviderModule: "route-proxy"}}})
kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "route-proxy_bound"})
if err != nil {
t.Fatal(err)
@@ -95,14 +95,14 @@ func routeRun(t *testing.T, module, id, path, templatePath, entryKey, firstEntry
}
// gitea's plan entry for its `web` route, as the controller writes it for a node whose public domain is example.org.
const giteaRoutePlanEntry = `{"provision":"route","provider":"novox","provider-module":"route-proxy","credential-form":"pair",` +
`"at":"novox.internal","from":"novox","as":"mesh_g14_gitea",` +
`"names":{"name-web":"git.example.org","internal-name-web":"git.novox.internal",` +
`"name-internal-api-refused":"git.example.org","internal-name-internal-api-refused":"git.novox.internal"}}`
const giteaRoutePlanEntry = `{"provision":"route","provider":"node-c","provider-module":"route-proxy","credential-form":"pair",` +
`"at":"node-c.internal","from":"node-c","as":"mesh_node-b_gitea",` +
`"names":{"name-web":"git.example.org","internal-name-web":"git.node-c.internal",` +
`"name-internal-api-refused":"git.example.org","internal-name-internal-api-refused":"git.node-c.internal"}}`
const keycloakRoutePlanEntry = `{"provision":"route","provider":"novox","provider-module":"route-proxy","credential-form":"pair",` +
`"at":"novox.internal","from":"novox","as":"mesh_g14_keycloak",` +
`"names":{"name":"keycloak.example.org","internal-name":"keycloak.novox.internal"}}`
const keycloakRoutePlanEntry = `{"provision":"route","provider":"node-c","provider-module":"route-proxy","credential-form":"pair",` +
`"at":"node-c.internal","from":"node-c","as":"mesh_node-b_keycloak",` +
`"names":{"name":"keycloak.example.org","internal-name":"keycloak.node-c.internal"}}`
func TestNatsGiteasOriginsAreFilledFromRouteProxysEntryForThePlanEntryTheControllerWrites(t *testing.T) {
e, _, _ := routeRun(t, "gitea", "origins", "/etc/gitea/origins", "../../../gitea/files/origins", "gitea.route",
@@ -123,8 +123,8 @@ func TestNatsKeycloaksHostnameIsFilledFromRouteProxysEntryForThePlanEntryTheCont
// A module that failed on a missing name is filled once the provider's entry carries it: the entry first held no
// names (as the provider wrote it before the controller composed them), then the controller's plan entry arrives.
func TestNatsAModuleThatFailedOnAMissingNameIsFilledWhenTheNameArrives(t *testing.T) {
bare := routeEntryFor(t, "gitea", `{"provision":"route","provider":"novox","provider-module":"route-proxy",`+
`"as":"mesh_g14_gitea"}`)
bare := routeEntryFor(t, "gitea", `{"provision":"route","provider":"node-c","provider-module":"route-proxy",`+
`"as":"mesh_node-b_gitea"}`)
e, b, heard := routeRun(t, "gitea", "origins", "/etc/gitea/origins", "../../../gitea/files/origins", "gitea.route", bare)
eventually(t, "the module fails for the missing name", func() bool {
r, _ := heard.last("gitea")
@@ -22,7 +22,7 @@ import (
// sealing to the key the caller sends, handing only what it allows that caller (the vault's own tests prove its
// rule), and saying `secret-changed` with no value.
// standInVault answers get on the bus, for provisioner.g14 alone and only the names it allows.
// standInVault answers get on the bus, for provisioner.node-b alone and only the names it allows.
type standInVault struct {
t *testing.T
conn *nats.Conn
@@ -155,8 +155,8 @@ func TestNatsAMovedModulesFileIsFilledWithItsSecretFromTheVault(t *testing.T) {
wire := sniff(t, b)
vault := startVault(t, b)
const token, password = "tok-3f9a1c-never-on-the-bus", "pw-77b2e0-never-on-the-bus"
vault.keep("own/g14/app/app-token", token, false)
vault.keep("own/g14/app/db-password", password, false)
vault.keep("own/node-b/app/app-token", token, false)
vault.keep("own/node-b/app/db-password", password, false)
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
b.config(t, "app", "font-size", "13")
b.assign(t, "app", app(t))
@@ -172,7 +172,7 @@ func TestNatsAMovedModulesFileIsFilledWithItsSecretFromTheVault(t *testing.T) {
})
eventually(t, "the needs name the secrets, never their values", func() bool {
got := b.keys(t, NeedsBucket, node+".app.")
return strings.Join(got, " ") == "g14.app.config.font-size g14.app.secret.app-token g14.app.secret.db-password"
return strings.Join(got, " ") == "node-b.app.config.font-size node-b.app.secret.app-token node-b.app.secret.db-password"
})
if wire.heard() == 0 {
t.Fatal("the sniffer heard nothing: it proves nothing")
@@ -182,9 +182,9 @@ func TestNatsAMovedModulesFileIsFilledWithItsSecretFromTheVault(t *testing.T) {
t.Errorf("the value crossed the bus in the clear: %q", said)
}
}
if vault.times("own/g14/app/app-token") != 1 || vault.times("own/g14/app/db-password") != 1 {
t.Errorf("the vault was asked %d and %d times, want once each", vault.times("own/g14/app/app-token"),
vault.times("own/g14/app/db-password"))
if vault.times("own/node-b/app/app-token") != 1 || vault.times("own/node-b/app/db-password") != 1 {
t.Errorf("the vault was asked %d and %d times, want once each", vault.times("own/node-b/app/app-token"),
vault.times("own/node-b/app/db-password"))
}
}
@@ -192,8 +192,8 @@ func TestNatsASecretRotatedInTheVaultReachesItsFileWithoutANewDeclaration(t *tes
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep("own/g14/app/app-token", "first", false)
vault.keep("own/g14/app/db-password", "pw", false)
vault.keep("own/node-b/app/app-token", "first", false)
vault.keep("own/node-b/app/db-password", "pw", false)
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
b.config(t, "app", "font-size", "13")
b.assign(t, "app", app(t))
@@ -204,7 +204,7 @@ func TestNatsASecretRotatedInTheVaultReachesItsFileWithoutANewDeclaration(t *tes
t.Fatal(err)
}
vault.keep("own/g14/app/app-token", "second", true)
vault.keep("own/node-b/app/app-token", "second", true)
eventually(t, "the rotated value reaches its file", func() bool {
return strings.Contains(e.read(t, appConf), "token = second\n")
})
@@ -223,13 +223,13 @@ func TestNatsASecretRotatedInTheVaultReachesItsFileWithoutANewDeclaration(t *tes
t.Errorf("the other secret's file moved %d times: only what changed is written", moved)
}
// What another machine's secret, or a secret no plan here names, changes is no reason to ask.
vault.keep("own/ace/app/app-token", "elsewhere", true)
vault.keep("own/g14/other/token", "unplanned", true)
vault.keep("own/node-a/app/app-token", "elsewhere", true)
vault.keep("own/node-b/other/token", "unplanned", true)
time.Sleep(300 * time.Millisecond)
if n := vault.times("own/g14/app/app-token"); n != 2 {
if n := vault.times("own/node-b/app/app-token"); n != 2 {
t.Errorf("the vault was asked for app-token %d times, want 2", n)
}
if vault.times("own/ace/app/app-token")+vault.times("own/g14/other/token") != 0 {
if vault.times("own/node-a/app/app-token")+vault.times("own/node-b/other/token") != 0 {
t.Error("a change of a secret this machine's plans do not name made the client ask")
}
}
@@ -238,8 +238,8 @@ func TestNatsASecretTheVaultDoesNotHoldYetIsWaitedFor(t *testing.T) {
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep("own/g14/app/db-password", "pw", false)
vault.allowed["own/g14/app/app-token"] = true // the plan names it; the vault does not hold it yet
vault.keep("own/node-b/app/db-password", "pw", false)
vault.allowed["own/node-b/app/app-token"] = true // the plan names it; the vault does not hold it yet
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
b.config(t, "app", "font-size", "13")
b.assign(t, "app", app(t))
@@ -255,7 +255,7 @@ func TestNatsASecretTheVaultDoesNotHoldYetIsWaitedFor(t *testing.T) {
}
// 1553: a file whose own values are all there is filled, though another file of the module waits.
eventually(t, "the secret's own file is filled while the template waits", func() bool { return e.read(t, appDB) == "pw" })
vault.keep("own/g14/app/app-token", "arrived", true)
vault.keep("own/node-b/app/app-token", "arrived", true)
eventually(t, "it is filled once the vault holds it", func() bool {
return strings.Contains(e.read(t, appConf), "token = arrived\n") && e.read(t, appDB) == "pw"
})
@@ -267,9 +267,9 @@ func TestNatsARefusalFailsTheModuleWithTheVaultsReasonAndIsNotAskedEveryPass(t *
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep("own/g14/app/app-token", "tok", false)
vault.keep("own/g14/app/db-password", "pw", false)
vault.refuse = "provisioner.g14 may not ask for own/g14/app/app-token: app's plan on g14 names no secret app-token"
vault.keep("own/node-b/app/app-token", "tok", false)
vault.keep("own/node-b/app/db-password", "pw", false)
vault.refuse = "provisioner.node-b may not ask for own/node-b/app/app-token: app's plan on node-b names no secret app-token"
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
b.config(t, "app", "font-size", "13")
b.assign(t, "app", app(t))
@@ -285,7 +285,7 @@ func TestNatsARefusalFailsTheModuleWithTheVaultsReasonAndIsNotAskedEveryPass(t *
time.Sleep(100 * time.Millisecond)
}
time.Sleep(200 * time.Millisecond)
if n := vault.times("own/g14/app/app-token"); n != 1 {
if n := vault.times("own/node-b/app/app-token"); n != 1 {
t.Errorf("a refused secret was asked %d times across several reconciles, want once", n)
}
if _, asked := e.counts(appConf); asked != 0 {
@@ -338,16 +338,16 @@ func TestAPlansSecretsAreReadStrictly(t *testing.T) {
const (
forgeFile = "/var/lib/claude-code/state/gitea-read.secret"
settingsFile = "/etc/claude-code/managed-settings.json"
forgeHolder = "provision/novox/gitea-read/" + node + "/claude-code"
forgeHolder = "provision/node-c/gitea-read/" + node + "/claude-code"
)
// claudeCode is claude-code's plan with the credential of what it requires, gitea-read from novox, kept in a file of
// claudeCode is claude-code's plan with the credential of what it requires, gitea-read from node-c, kept in a file of
// its own and named in a template.
func claudeCode(t *testing.T) Plan {
tpl, _ := templates(t, map[string]string{"managed-settings.json": `{"forge":"${secret:gitea-read}"}`})
return Plan{Templates: tpl, Files: []PlannedFile{{ID: "settings", Path: settingsFile,
Template: "managed-settings.json", OwnerAtRuntime: "owned"}},
Provisions: []PlannedProvision{{Name: "gitea-read", Provision: "gitea-read", Provider: "novox",
Provisions: []PlannedProvision{{Name: "gitea-read", Provision: "gitea-read", Provider: "node-c",
Path: forgeFile}}}
}
@@ -356,8 +356,8 @@ func TestNatsAModulesRequiredCredentialIssuedForItIsFilledFromTheVaultAndFollows
heard := b.listen(t)
wire := sniff(t, b)
vault := startVault(t, b)
const first = "https://agent-g14:gta-first-never-on-the-bus@forge.example"
const second = "https://agent-g14:gta-second-never-on-the-bus@forge.example"
const first = "https://agent-node-b:gta-first-never-on-the-bus@forge.example"
const second = "https://agent-node-b:gta-second-never-on-the-bus@forge.example"
vault.keep(forgeHolder, first, false)
e := startEngine(t, map[string]string{forgeFile: "owned", settingsFile: "owned"})
b.assign(t, "claude-code", claudeCode(t))
@@ -378,13 +378,13 @@ func TestNatsAModulesRequiredCredentialIssuedForItIsFilledFromTheVaultAndFollows
return e.read(t, forgeFile) == second
})
// Another machine's or another module's holder value changing is no reason to ask.
vault.keep("provision/novox/gitea-read/ace/claude-code", "elsewhere", true)
vault.keep("provision/novox/gitea-read/"+node+"/zsh", "unplanned", true)
vault.keep("provision/node-c/gitea-read/node-a/claude-code", "elsewhere", true)
vault.keep("provision/node-c/gitea-read/"+node+"/zsh", "unplanned", true)
time.Sleep(300 * time.Millisecond)
if n := vault.times(forgeHolder); n != 2 {
t.Errorf("the vault was asked for the holder value %d times, want 2", n)
}
if vault.times("provision/novox/gitea-read/ace/claude-code")+vault.times("provision/novox/gitea-read/"+node+"/zsh") != 0 {
if vault.times("provision/node-c/gitea-read/node-a/claude-code")+vault.times("provision/node-c/gitea-read/"+node+"/zsh") != 0 {
t.Error("a change of a value no plan here names made the client ask")
}
if wire.heard() == 0 {
@@ -402,8 +402,8 @@ func TestNatsARequiredCredentialTheVaultRefusesFailsTheModule(t *testing.T) {
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep(forgeHolder, "https://agent-g14:tok@forge.example", false)
vault.refuse = "provisioner.g14 may not ask for " + forgeHolder + ": claude-code's plan on g14 names no credential of gitea-read from novox"
vault.keep(forgeHolder, "https://agent-node-b:tok@forge.example", false)
vault.refuse = "provisioner.node-b may not ask for " + forgeHolder + ": claude-code's plan on node-b names no credential of gitea-read from node-c"
e := startEngine(t, map[string]string{forgeFile: "owned", settingsFile: "owned"})
b.assign(t, "claude-code", claudeCode(t))
aClient(t, b, e, t.TempDir())
@@ -423,12 +423,12 @@ func TestAPlansProvisionsAreReadStrictly(t *testing.T) {
Files: []PlannedFile{{ID: "conf", Path: appConf, Template: "app.conf", OwnerAtRuntime: "owned"}},
Secrets: []PlannedSecret{{Name: "own"}}}
}
good := PlannedProvision{Name: "gitea-read", Provision: "gitea-read", Provider: "novox", Path: forgeFile}
good := PlannedProvision{Name: "gitea-read", Provision: "gitea-read", Provider: "node-c", Path: forgeFile}
for why, change := range map[string]func(*PlannedProvision){
"a name that is not a secret's": func(p *PlannedProvision) { p.Name = "Gitea Read" },
"a name an own secret has": func(p *PlannedProvision) { p.Name = "own" },
"no provision": func(p *PlannedProvision) { p.Provision = "" },
"a provider that is no machine": func(p *PlannedProvision) { p.Provider = "../ace" },
"a provider that is no machine": func(p *PlannedProvision) { p.Provider = "../node-a" },
"no provider": func(p *PlannedProvision) { p.Provider = "" },
"a relative path": func(p *PlannedProvision) { p.Path = "var/x" },
"a template's path": func(p *PlannedProvision) { p.Path = appConf },
@@ -455,14 +455,14 @@ func TestAPlansProvisionsAreReadStrictly(t *testing.T) {
const (
dbFile = "/var/lib/nextcloud/state/database.secret"
dbConf = "/etc/nextcloud/config.php"
dbHolder = "pair/novox/postgres-database/" + node + "/nextcloud"
dbHolder = "pair/node-c/postgres-database/" + node + "/nextcloud"
)
func nextcloud(t *testing.T) Plan {
tpl, _ := templates(t, map[string]string{"config.php": `dbpass=${secret:database}`})
return Plan{Templates: tpl, Files: []PlannedFile{{ID: "config", Path: dbConf, Template: "config.php",
OwnerAtRuntime: "owned"}},
Provisions: []PlannedProvision{{Name: "database", Provision: "postgres-database", Provider: "novox",
Provisions: []PlannedProvision{{Name: "database", Provision: "postgres-database", Provider: "node-c",
ProviderModule: "postgres", CredentialForm: "pair", Path: dbFile}}}
}
@@ -494,7 +494,7 @@ func TestNatsAModulesOwnLoginFromAPerLoginProviderIsFilledFromTheVaultAndFollows
return e.read(t, dbFile) == second
})
// Another consumer's login changing is no reason to ask.
other := "pair/novox/postgres-database/ace/nextcloud"
other := "pair/node-c/postgres-database/node-a/nextcloud"
vault.keep(other, "elsewhere", true)
time.Sleep(300 * time.Millisecond)
if n := vault.times(dbHolder); n != 2 {
@@ -511,7 +511,7 @@ func TestNatsAModulesOwnLoginFromAPerLoginProviderIsFilledFromTheVaultAndFollows
}
func TestAConsumersLoginIsNamedWithItsLocalName(t *testing.T) {
if got := PairName("novox", "postgres-database", "g14", "umami", "stats"); got != "pair/novox/postgres-database/g14/umami/stats" {
if got := PairName("node-c", "postgres-database", "node-b", "umami", "stats"); got != "pair/node-c/postgres-database/node-b/umami/stats" {
t.Errorf("a login under a local name: %s", got)
}
}
@@ -522,8 +522,8 @@ func TestNatsAKnownSecretsFileIsFilledWhileATemplateWaits1553(t *testing.T) {
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep("own/g14/app/app-token", "tok", false)
vault.keep("own/g14/app/db-password", "pw", false)
vault.keep("own/node-b/app/app-token", "tok", false)
vault.keep("own/node-b/app/db-password", "pw", false)
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
// No config font-size: the template waits for it.
b.assign(t, "app", app(t))
@@ -546,8 +546,8 @@ func TestNatsATemplateWithAWaitingValueKeepsItsFileAsItIs1553(t *testing.T) {
b := startBus(t)
heard := b.listen(t)
vault := startVault(t, b)
vault.keep("own/g14/app/app-token", "tok", false)
vault.keep("own/g14/app/db-password", "pw", false)
vault.keep("own/node-b/app/app-token", "tok", false)
vault.keep("own/node-b/app/db-password", "pw", false)
e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"})
b.assign(t, "app", app(t))
aClient(t, b, e, t.TempDir())
@@ -10,14 +10,14 @@ accounts {
jetstream: enabled
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-controller_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_needs.*.$KV.mesh-provisioner_needs.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$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-controller_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_needs.>", "$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-controller_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_needs.$KV.mesh-provisioner_needs.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-controller_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_needs", "$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-controller_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_needs.>", "$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.>", "$KV.mesh-provisioner_needs.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"] }
{ user: "provisioner.node-b", password: "x", permissions: {
publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_needs.*.$KV.mesh-provisioner_needs.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.node-b.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.node-b.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.node-b.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.node-b.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_needs.>", "$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.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-controller_assignments.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_needs.$KV.mesh-provisioner_needs.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.node-b.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.node-b.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.node-b.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.node-b.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-controller_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_needs", "$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-controller_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_needs.>", "$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.node-b.>", "$KV.mesh-provisioner_collect.node-b.>", "$KV.mesh-provisioner_needs.node-b.>", "mesh.seat.mesh-vault.call.get.provisioner~node-b", "prov.applied.node-b.>", "prov.changed.node-b.>"] }
subscribe: { allow: ["_INBOX.provisioner.node-b.>", "mesh.mod.mesh-vault.event.secret-changed", "prov.ask.node-b"] }
allow_responses: { max: 1, ttl: "1m" }
} }
{ user: "g14.node-tools", password: "x", permissions: {
publish: { allow: ["$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.g14.*", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.>", "mesh.control.g14.tools-alive", "mesh.mod.*.call.*.*.g14~node-tools", "mesh.mod.*.call.*.g14~node-tools", "mesh.mod.*.tool.>", "mesh.seat.*.call.*.*.g14~node-tools", "mesh.seat.*.call.*.g14~node-tools", "mesh.seat.*.tool.>", "prov.ask.g14"], deny: ["mesh.seat.node-launcher.tool.secret", "mesh.seat.node-launcher.tool.secret.*", "mesh.mod.*.tool.node-launcher.secret", "mesh.mod.*.tool.node-launcher.secret.*", "mesh.seat.node-launcher.call.secret.>", "mesh.mod.*.call.node-launcher.secret.>"] }
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.claude-code", "$SRV.INFO.claude-code.>", "$SRV.INFO.node-tools", "$SRV.INFO.node-tools.>", "$SRV.PING", "$SRV.PING.claude-code", "$SRV.PING.claude-code.>", "$SRV.PING.node-tools", "$SRV.PING.node-tools.>", "$SRV.STATS", "$SRV.STATS.claude-code", "$SRV.STATS.claude-code.>", "$SRV.STATS.node-tools", "$SRV.STATS.node-tools.>", "_INBOX.g14.node-tools.>", "mesh.assignment.g14.*", "mesh.mod.claude-code.call.>", "mesh.mod.claude-code.tool.>", "prov.changed.g14.>"] }
{ user: "node-b.node-tools", password: "x", permissions: {
publish: { allow: ["$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.node-b.*", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.>", "mesh.control.node-b.tools-alive", "mesh.mod.*.call.*.*.node-b~node-tools", "mesh.mod.*.call.*.node-b~node-tools", "mesh.mod.*.tool.>", "mesh.seat.*.call.*.*.node-b~node-tools", "mesh.seat.*.call.*.node-b~node-tools", "mesh.seat.*.tool.>", "prov.ask.node-b"], deny: ["mesh.seat.node-launcher.tool.secret", "mesh.seat.node-launcher.tool.secret.*", "mesh.mod.*.tool.node-launcher.secret", "mesh.mod.*.tool.node-launcher.secret.*", "mesh.seat.node-launcher.call.secret.>", "mesh.mod.*.call.node-launcher.secret.>"] }
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.claude-code", "$SRV.INFO.claude-code.>", "$SRV.INFO.node-tools", "$SRV.INFO.node-tools.>", "$SRV.PING", "$SRV.PING.claude-code", "$SRV.PING.claude-code.>", "$SRV.PING.node-tools", "$SRV.PING.node-tools.>", "$SRV.STATS", "$SRV.STATS.claude-code", "$SRV.STATS.claude-code.>", "$SRV.STATS.node-tools", "$SRV.STATS.node-tools.>", "_INBOX.node-b.node-tools.>", "mesh.assignment.node-b.*", "mesh.mod.claude-code.call.>", "mesh.mod.claude-code.tool.>", "prov.changed.node-b.>"] }
allow_responses: { max: 1, ttl: "1m" }
} }
]
@@ -16,24 +16,24 @@ func TestAPlanProvisionWithEveryFieldTheControllerWritesIsRead(t *testing.T) {
if err != nil {
t.Fatal(err)
}
provision := `"provisions":[{"provision":"route","local":"web","provider":"novox","provider-module":"route-proxy",` +
`"credential-form":"pair","contributes":{"label":"git"},"at":"10.10.0.1","from":"novox","as":"gitea",` +
`"names":{"name-web":"git.example.org"}},{"name":"gitea-read","provision":"gitea-read","provider":"novox",` +
provision := `"provisions":[{"provision":"route","local":"web","provider":"node-c","provider-module":"route-proxy",` +
`"credential-form":"pair","contributes":{"label":"git"},"at":"10.10.0.1","from":"node-c","as":"gitea",` +
`"names":{"name-web":"git.example.org"}},{"name":"gitea-read","provision":"gitea-read","provider":"node-c",` +
`"provider-module":"gitea","credential-form":"issued","credential-tool":"gitea_agent_token",` +
`"path":"/var/lib/claude-code/gitea-read.secret"}],"items":`
value := fmt.Sprintf(`{"node":"g14","module":"mesh-cli","provisioning":%s}`,
value := fmt.Sprintf(`{"node":"node-b","module":"mesh-cli","provisioning":%s}`,
strings.Replace(string(p), `"items":`, provision, 1))
_, got, err := planOf("g14", "g14.mesh-cli", []byte(value))
_, got, err := planOf("node-b", "node-b.mesh-cli", []byte(value))
if err != nil {
t.Fatalf("a plan with the controller's fields is refused: %v", err)
}
if len(got.Provisions) != 2 || got.Provisions[0].At != "10.10.0.1" || got.Provisions[0].From != "novox" ||
if len(got.Provisions) != 2 || got.Provisions[0].At != "10.10.0.1" || got.Provisions[0].From != "node-c" ||
got.Provisions[0].As != "gitea" || got.Provisions[0].Names["name-web"] != "git.example.org" ||
got.Provisions[1].CredentialTool != "gitea_agent_token" {
t.Fatalf("the plan's provisions read as %+v", got.Provisions)
}
spoilt := strings.Replace(value, `"at":`, `"surprise":1,"at":`, 1)
if _, _, err := planOf("g14", "g14.mesh-cli", []byte(spoilt)); err == nil {
if _, _, err := planOf("node-b", "node-b.mesh-cli", []byte(spoilt)); err == nil {
t.Fatalf("a provision with a field nobody writes is read: %s", spoilt)
}
}
@@ -45,7 +45,7 @@ func rig(wait time.Duration) (*Service, *fakeBus) {
return &Service{Asks: b, Now: time.Now, Wait: wait, Poll: time.Millisecond}, b
}
const get, put = "http://novox.internal:20001/b/doc.docx?X-Amz-Signature=a", "http://novox.internal:20001/b/doc.pdf?X-Amz-Signature=b"
const get, put = "http://node-c.internal:20001/b/doc.docx?X-Amz-Signature=a", "http://node-c.internal:20001/b/doc.pdf?X-Amz-Signature=b"
// 1595/AC1: one ask with those fields is published on convert-asks, and the verb answers what the converter wrote under
// the ask's id.
@@ -68,7 +68,7 @@ func TestTheToolSideAndTheConverterConvertOverARealBus(t *testing.T) {
Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardNew, MaxAge: 24 * time.Hour, MaxBytes: 8 << 20}); err != nil {
t.Fatal(err)
}
account := process.Account{URL: s.ClientURL(), Node: "novox", Module: "only-office", User: "novox.only-office.converter", Password: "pw"}
account := process.Account{URL: s.ClientURL(), Node: "node-c", Module: "only-office", User: "node-c.only-office.converter", Password: "pw"}
if _, err := js.CreateConsumer(ctx, AsksStream, jetstream.ConsumerConfig{Durable: account.QueueConsumerName(),
AckPolicy: jetstream.AckExplicitPolicy, AckWait: 30 * time.Second, MaxDeliver: 3}); err != nil {
t.Fatal(err)
@@ -85,7 +85,7 @@ func TestTheToolSideAndTheConverterConvertOverARealBus(t *testing.T) {
go runConverter(runCtx, account, w)
svc := &Service{Asks: natsBus{js: js, kv: kv}, Now: time.Now, Wait: 20 * time.Second, Poll: 50 * time.Millisecond}
got, err := svc.Convert(ctx, "http://novox.internal:20001/b/in.docx?X-Amz-Signature=a", "docx", tgt.srv.URL+"/b/in.pdf?sig=1", "")
got, err := svc.Convert(ctx, "http://node-c.internal:20001/b/in.docx?X-Amz-Signature=a", "docx", tgt.srv.URL+"/b/in.pdf?sig=1", "")
if err != nil {
t.Fatal(err)
}
@@ -101,7 +101,7 @@ func worker(d *fakeDocs) (*converterWorker, *answers) {
func askBody(t *testing.T, target string, sha string) []byte {
raw, err := json.Marshal(Ask{ID: "ask1", Op: OpConvert, At: time.Now(), Deadline: time.Now().Add(time.Minute),
Source: "http://novox.internal:20001/b/in.docx?X-Amz-Signature=a", Type: "docx", Target: target, SHA256: sha})
Source: "http://node-c.internal:20001/b/in.docx?X-Amz-Signature=a", Type: "docx", Target: target, SHA256: sha})
if err != nil {
t.Fatal(err)
}
@@ -142,7 +142,7 @@ func TestTheConverterPostsTheRequestAndPutsThePDF(t *testing.T) {
t.Fatalf("%d conversion requests", len(d.requests))
}
r := d.requests[0]
if r["url"] != "http://novox.internal:20001/b/in.docx?X-Amz-Signature=a" || r["filetype"] != "docx" || r["outputtype"] != "pdf" ||
if r["url"] != "http://node-c.internal:20001/b/in.docx?X-Amz-Signature=a" || r["filetype"] != "docx" || r["outputtype"] != "pdf" ||
r["async"] != false || r["key"] != strings.Repeat("ab", 32) || d.accept != "application/json" {
t.Fatalf("request %v accept %q", r, d.accept)
}
+2 -2
View File
@@ -120,7 +120,7 @@ and a fake link reader in the tests.
## What changes when it is assigned
| | g14 | shanks |
| | node-b | node-d |
|---|---|---|
| package | none: `slack-desktop` 4.51.191-1, AUR, installed explicitly | the same |
| start | i3's configuration gains the module's start. **Until the predecessor's `~/.config/autostart/slack.desktop` is deleted, the next login starts Slack twice.** The second start hands over to the first and exits, because Slack is single-instance. Running now: started by dex from that entry at the login of 2026-10-04 16:26 | the same entry, the same until deleted. Running now: since 2026-10-04 17:05, **with its stdout on a pipe nobody reads** (that session predates the `i3` module's login entry), so `slack_check` names it until `slack_restart` or the next login |
@@ -135,7 +135,7 @@ and a fake link reader in the tests.
off. Leave the setting off: ticking it makes the entry again, and `slack_check` names it.
2. **Delete `~/.config/i3/config.d/50-slack.conf`.** It is the operator's own file, and its rules are
now the module's.
3. **shanks only:** run `slack_restart`, or log out and in, so that Slack's output is read.
3. **node-d only:** run `slack_restart`, or log out and in, so that Slack's output is read.
`slack_check` then answers `ok`. Deleting both files before the module is assigned would leave Slack
without a start and without its rules until it is.
+1 -1
View File
@@ -51,7 +51,7 @@ func TestTheDropInGrantsExactlyTheOperatorAccountAndParses(t *testing.T) {
if err != nil {
t.Skip("visudo is not installed here; the rendered drop-in is not checked")
}
for _, account := range []string{"operator", "ace", "jochen-s"} {
for _, account := range []string{"operator", "node-a", "jochen-s"} {
file := filepath.Join(t.TempDir(), "10-mesh-operator")
rendered := strings.ReplaceAll(content, "${machine:account}", account)
if err := os.WriteFile(file, []byte(rendered), 0o440); err != nil {
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"testing"
)
const listNovox = `Matching Defaults entries for operator on anchor:
const listAnchor = `Matching Defaults entries for operator on anchor:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/bin
User operator may run the following commands on anchor:
@@ -14,7 +14,7 @@ User operator may run the following commands on anchor:
`
func TestSudoListIsParsedIntoDefaultsAndRules(t *testing.T) {
r := ParseList(listNovox, "x")
r := ParseList(listAnchor, "x")
if r.Account != "operator" || r.Host != "anchor" {
t.Fatalf("who: %+v", r)
}