diff --git a/cmd/mesh-controller/issued_to_vault.go b/cmd/mesh-controller/issued_to_vault.go index fb59b127..e0c223bd 100644 --- a/cmd/mesh-controller/issued_to_vault.go +++ b/cmd/mesh-controller/issued_to_vault.go @@ -2,9 +2,15 @@ package main import ( "context" + "errors" "fmt" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/inventory" ) @@ -15,22 +21,19 @@ import ( // controller mints such an account and writes its hash to the bus's user list, as before, and then hands the whole // value at once to the vault's `accept` with origin `issued`: sealed here to the key the vault made for that call, so // the vault alone opens it (ADR 0349 clause 8). The vault keeps it as the module's own secret on that machine and -// raises `secret-changed`; a re-issue is the same hand with a new value, which replaces the old in place. An account -// not covered is minted and kept as it was and handed to nobody. +// raises `secret-changed`; a re-issue is the same hand with a new value, which replaces the old in place. Until issue 774 an +// account not covered was minted and kept as it was and handed to nobody. // -// **Since issue 774 an installed vault mints every account itself** (issueAccount forwards to its issue-account), so -// this hand is reached only where the controller still mints: before a vault is installed (where it hands nothing) and +// **Since issue 774 every account the controller mints is handed, covered or not**, once a vault is installed: until +// the grants part's take-over the controller still mints (the bus loads its list), and a vault that holds every value +// at once needs no node to report a hash for it to adopt (ADR 0547 clause 9). From the take-over the vault mints every +// account itself (issueAccount forwards to its issue-account), so this hand is reached only before it, and by // `rollout mint`'s re-issue of a module's account onto a new bus. The vault never lets such a hand replace an account it // minted. // vaultAskerForIssue is the vault as `module issue` asks it. A test replaces it. var vaultAskerForIssue = func(ctx context.Context) vaultAsker { return theVault(ctx) } -// accountIsCovered says whether the bus accounts minted for m are the vault's to hold (ADR 0374 clause 1). -func accountIsCovered(m catalogue.Manifest) bool { - return catalogue.CarriesVaultSealed(m.Module) || m.Moved -} - // vaultInstalled says whether the vault is assigned to any machine. Until it is, as in genesis (ADR 0113 §15 to §18, // mesh-host's bootstrap, which issues the bus's and the provisioner's accounts before the vault step), there is no // vault to hand to: the value is kept in the controller's store as before, and the vault takes it afterwards by the @@ -49,13 +52,45 @@ func vaultInstalled(ctx context.Context, inv *inventory.Inventory) (bool, error) return false, nil } -// handIssuedToVault hands the value just minted for module's own secret name on node to the vault, when the module's -// accounts are covered, and does nothing otherwise. Loud when the vault does not take it: the bus's user list -// already holds the new hash, so a vault that kept the old value would serve a password that no longer logs in. -func handIssuedToVault(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest, node, name, held string) error { - if !accountIsCovered(m) { +// grantsTookOver says whether the grants part took over the bus's user list: its key `user-list.sealed` says +// FollowGrants (novox/hq ADR 0547 clauses 7 and 8), which the bus node's engine follows. No bucket or no key yet is no +// take-over; a bus that cannot be read is an error, never a guess. A test replaces it. +var grantsTookOver = func(ctx context.Context) (bool, error) { + over := false + err := onTheBus(func(conn *nats.Conn) error { + js, err := jetstream.New(conn) + if err != nil { + return err + } + kv, err := js.KeyValue(ctx, broker.GrantsUserListBucket) + if errors.Is(err, jetstream.ErrBucketNotFound) { + return nil + } else if err != nil { + return err + } + e, err := kv.Get(ctx, broker.GrantsUserListKey) + if errors.Is(err, jetstream.ErrKeyNotFound) { + return nil + } else if err != nil { + return err + } + g, err := sdkdelivery.DecodeGrantsUserList(e.Value()) + if err != nil { + return err + } + over = g.Follow == sdkdelivery.FollowGrants return nil + }) + if err != nil { + return false, fmt.Errorf("whether the grants part took over the bus's user list cannot be read: %w", err) } + return over, nil +} + +// handIssuedToVault hands the value just minted for module's own secret name on node to the vault, once one is +// installed, covered or not (issue 774), and does nothing before. Loud when the vault does not take it: the bus's user +// list already holds the new hash, so a vault that kept the old value would serve a password that no longer logs in. +func handIssuedToVault(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest, node, name, held string) error { if installed, err := vaultInstalled(ctx, inv); err != nil { return fmt.Errorf("whether the vault is installed cannot be read, so %s of %s was not handed to it: %w", name, m.Module, err) diff --git a/cmd/mesh-controller/issued_to_vault_test.go b/cmd/mesh-controller/issued_to_vault_test.go index 38edc1cc..13e51c5f 100644 --- a/cmd/mesh-controller/issued_to_vault_test.go +++ b/cmd/mesh-controller/issued_to_vault_test.go @@ -8,21 +8,50 @@ import ( "strings" "testing" + "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-controller/internal/broker" + "github.com/novox/mesh-controller/internal/testbus" ) -// Every other test mints without a vault to hand to: a stand-in that takes anything, replaced by the tests below. +// grantsTookOverOnTheBus is the reader of the grants part's take-over a test reads on its own bus. +var grantsTookOverOnTheBus = grantsTookOver + +// Every other test mints without a vault to hand to: a stand-in that takes anything, replaced by the tests below; and +// before the grants part's take-over, which no test bus says. func init() { vaultAskerForIssue = func(context.Context) vaultAsker { return newStandInVault(nil).ask } + grantsTookOver = func(context.Context) (bool, error) { return false, nil } } -// A covered account is handed to the vault when it is minted (novox/hq ADR 0374, issue 787): the whole value, sealed -// to the key the vault made for the call, with origin issued. A module that is neither core nor moved hands nothing. -func TestModuleIssueHandsACoveredAccountToTheVaultAndOnlyThat(t *testing.T) { +// theGrantsPartTookOver says the grants part's take-over is on, or off, for the rest of the test. +func theGrantsPartTookOver(t *testing.T, over bool) { + t.Helper() + was := grantsTookOver + grantsTookOver = func(context.Context) (bool, error) { return over, nil } + t.Cleanup(func() { grantsTookOver = was }) +} + +// An account is handed to the vault when it is minted (novox/hq ADR 0374, issue 787): the whole value, sealed to the +// key the vault made for the call, with origin issued. Since issue 774 every account is, covered or not, until the +// grants part's take-over, read on the bus as the bus node reads it; from the take-over nothing is minted here and the +// issue is forwarded to the vault's issue-account. +func TestModuleIssueHandsEveryAccountToTheVaultUntilTheTakeOver(t *testing.T) { open := aMesh(t) ctx := t.Context() theVaultIsInstalled(t, open) + js, err := broker.Dial(testbus.URL(t)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(js.Close) + servingBus.Store(js) + t.Cleanup(func() { servingBus.Store(nil) }) + was := grantsTookOver + grantsTookOver = grantsTookOverOnTheBus + t.Cleanup(func() { grantsTookOver = was }) inv := open.inventory known := broker.Broker{Fingerprint: "ab"} @@ -55,27 +84,22 @@ func TestModuleIssueHandsACoveredAccountToTheVaultAndOnlyThat(t *testing.T) { issue(core, "first-core-password") issue(moved, "first-moved-password") issue(plain, "kept-password") - // Issue 774: with the vault installed an issue mints nothing here; it is forwarded to the vault's issue-account. if err := issueProvisionerClient(ctx, inv, core, "laptop", known, "10.0.0.1:4222"); err != nil { t.Fatal(err) } - if _, held, err := inv.ModuleSecretIfIssued(ctx, "laptop", "nats", broker.ProvisionerSecret); err != nil || held { - t.Errorf("the controller kept a value of its own for the client's account (held %v, %v)", held, err) - } said := vault.said() - client := broker.Principal{Kind: broker.KindProvisioner, Node: "laptop", Module: "nats"}.Username() - for _, want := range []string{`"module":"nats"`, `"module":"moved-bot"`, `"name":"broker"`, `"node":"laptop"`, - `"verb":"issue-account"`, `"user":"` + client + `"`} { + for _, want := range []string{`"module":"nats"`, `"module":"moved-bot"`, `"name":"broker"`, + `"name":"` + broker.ProvisionerSecret + `"`, `"node":"laptop"`} { if !strings.Contains(said, want) { t.Errorf("the vault was told %s, without %s", said, want) } } - if n := strings.Count(said, `"origin":"issued"`); n != 2 { - t.Errorf("%d accounts were handed with origin issued, want 2 (the core's and the moved module's): %s", n, said) + if n := strings.Count(said, `"origin":"issued"`); n != 4 { + t.Errorf("%d accounts were handed with origin issued, want 4 (the core's two, the moved and the plain module's): %s", n, said) } - if strings.Contains(said, "kept-bot") { - t.Errorf("an account that is neither core nor moved was handed to the vault: %s", said) + if !strings.Contains(said, "kept-bot") { + t.Errorf("an account that is neither core nor moved was not handed to the vault: %s", said) } if strings.Contains(said, "first-core-password") || strings.Contains(said, "first-moved-password") { t.Errorf("a value crossed the bus in the clear: %s", said) @@ -88,6 +112,38 @@ func TestModuleIssueHandsACoveredAccountToTheVaultAndOnlyThat(t *testing.T) { !strings.Contains(got, `"url":"nats://10.0.0.1:4222"`) || !strings.Contains(got, `"fingerprint":"ab"`) { t.Errorf("the vault was handed %q after a re-issue, not the new account whole", got) } + + // The take-over: the grants part's key says the bus node follows its list. From then the vault mints. + api, err := jetstream.New(js.Conn()) + if err != nil { + t.Fatal(err) + } + kv, err := api.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: broker.GrantsUserListBucket}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = api.DeleteKeyValue(context.Background(), broker.GrantsUserListBucket) }) + client := broker.Principal{Kind: broker.KindProvisioner, Node: "laptop", Module: "nats"}.Username() + for _, follow := range []string{sdkdelivery.FollowController, sdkdelivery.FollowGrants} { + value, err := sdkdelivery.EncodeGrantsUserList(sdkdelivery.GrantsUserList{Follow: follow}) + if err != nil { + t.Fatal(err) + } + if _, err := kv.Put(ctx, broker.GrantsUserListKey, value); err != nil { + t.Fatal(err) + } + before, _, _ := inv.BusUserHash(ctx, client) + if err := issueProvisionerClient(ctx, inv, core, "laptop", known, "10.0.0.1:4222"); err != nil { + t.Fatal(err) + } + after, _, _ := inv.BusUserHash(ctx, client) + if minted := before != after; minted != (follow == sdkdelivery.FollowController) { + t.Errorf("following %s's list, the controller minted %s: %v", follow, client, minted) + } + } + if said := vault.said(); strings.Count(said, `"verb":"issue-account"`) != 1 || !strings.Contains(said, `"user":"`+client+`"`) { + t.Errorf("the vault was not asked once, after the take-over, to issue %s: %s", client, said) + } } // A vault that does not take the value fails the issue loudly: the bus's user list holds the new hash already. @@ -161,8 +217,4 @@ func TestModuleIssueBeforeTheVaultIsInstalledKeepsTheValueAndHandsNothing(t *tes if err := issueWith(ctx, inv, core, "laptop", "", known, "10.0.0.1:4222", "module.nats.laptop", "pw2"); err == nil { t.Error("a vault that is installed and does not answer did not fail the issue") } - if err := issueProvisionerClient(ctx, inv, core, "laptop", known, "10.0.0.1:4222"); err == nil || - !strings.Contains(err.Error(), "one issuer") { - t.Errorf("`module issue` forwarded to a vault that does not answer: %v", err) - } } diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index 3f1b0c9f..c355e753 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -1019,17 +1019,17 @@ const issueLockedFor = 2 * time.Minute // their look, mint and seal: the hash the bus holds is always the password sealed. A seal that fails leaves the // account unsealed, issued again by the next issue. // -// **Where a vault is installed the controller mints nothing** (issue 774, research 072's gap "vault mints after the -// lease"; the controller is deprecated and hands its duties over): the vault is the one issuer of every account the -// grants part wants, minted on that want, so the issue is forwarded to the vault's issue-account and only after runs -// (what the account needs beside its value: its durable consumers). The controller's store keeps no value for it and its -// own user list no hash: no account waits on a node reporting here for the vault to take it. Until a vault is installed -// (genesis, ADR 0113 §15 to §18) the controller mints as before. +// **From the grants part's take-over the controller mints nothing** (issue 774, research 072's gap "vault mints after +// the lease"; the controller is deprecated and hands its duties over): the bus then loads the grants part's list, whose +// hashes are the vault's, so the issue is forwarded to the vault's issue-account and only after runs (what the account +// needs beside its value: its durable consumers). **Before it** the bus loads the controller's own list, whose hashes +// come from its store alone, so an account the vault minted would be refused: the controller mints as before and hands +// every account it mints to an installed vault at once (handIssuedToVault), so none waits on a node reporting its hash. func issueAccount(ctx context.Context, accounts accountIssuer, u inventory.BusUser, seal func(password string) error, after func() error) error { only, _ := ctx.Value(mintOnlyIfAbsent{}).(bool) if ask, installed, err := vaultIssuer(ctx, accounts); err != nil { - return fmt.Errorf("whether the vault is installed cannot be read, so %s is not issued: %w", u.Username, err) + return fmt.Errorf("whether the vault issues bus accounts yet cannot be read, so %s is not issued: %w", u.Username, err) } else if installed { return forwardToTheVault(ctx, ask, u.Username, only, after) } @@ -1075,8 +1075,9 @@ func issueAccount(ctx context.Context, accounts accountIssuer, u inventory.BusUs return nil } -// vaultIssuer answers the vault to forward an issue to, and whether one is installed (vaultInstalled); a store that is -// not the controller's inventory (a test's) has none. A test replaces it. +// vaultIssuer answers the vault to forward an issue to, and whether the issue is the vault's: one is installed +// (vaultInstalled) and the grants part took over the bus's user list (grantsTookOver). A store that is not the +// controller's inventory (a test's) has none. A test replaces it. var vaultIssuer = func(ctx context.Context, accounts accountIssuer) (vaultAsker, bool, error) { inv, ok := accounts.(*inventory.Inventory) if !ok { @@ -1086,6 +1087,9 @@ var vaultIssuer = func(ctx context.Context, accounts accountIssuer) (vaultAsker, if err != nil || !installed { return nil, false, err } + if over, err := grantsTookOver(ctx); err != nil || !over { + return nil, false, err + } return vaultAskerForIssue(ctx), true, nil } diff --git a/cmd/mesh-controller/process_issue_integration_test.go b/cmd/mesh-controller/process_issue_integration_test.go index a319e2e7..5650ab37 100644 --- a/cmd/mesh-controller/process_issue_integration_test.go +++ b/cmd/mesh-controller/process_issue_integration_test.go @@ -19,16 +19,18 @@ import ( // // review round 1, findings 3 and 4 (1181/AC1, AC2): module issue mints and delivers a process's own // account against the store — no bus to dial, busAddress "" — for mesh-mail's real manifest shape (review -// round 1 finding 1: no own secret `broker` at all, its tools served by the tool runner), as long as no vault is -// installed (genesis). Issue 774: once a vault is installed, the controller mints nothing: the issue is forwarded -// to the vault's issue-account for the process's user, and the controller's hash and value stay as they were. +// round 1 finding 1: no own secret `broker` at all, its tools served by the tool runner), and issuing it +// again is the path every bus account takes (`rollout mint`'s KindProcess case, and a re-run of `module +// issue`): it rotates the password, and the old hash no longer matches. Issue 774: every account minted is handed to the +// vault, covered or not, until the grants part's take-over; from it the issue is forwarded to the vault's issue-account, +// and the controller's hash stays as it was. func TestModuleIssueMintsAndDeliversAProcesssAccount(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory + theVaultIsInstalled(t, open) m := mailSenderWithProcess() - m.Moved = true register(t, open, m) vault := newStandInVault(t) @@ -57,24 +59,39 @@ func TestModuleIssueMintsAndDeliversAProcesssAccount(t *testing.T) { if err != nil || !held || hash1 == "" { t.Fatalf("the process's account was not minted: held %v, %v", held, err) } - // 1181/AC1: kept in the provisioner client's JSON — the url and the fingerprint being exactly what issueProcess - // was given above, not a guess at what it computes. - sealed, held, err := inv.ModuleSecretIfIssued(ctx, "laptop", "mesh-mail", "broker-send") - if err != nil || !held || sealed == "" { - t.Fatalf("the process's account is not kept as broker-send: held %v, %v", held, err) - } - if said := vault.said(); said != "" { - t.Errorf("the vault was asked before it was installed: %s", said) - } - // Issue 774: with the vault installed, forwarded; nothing minted or rotated here. - theVaultIsInstalled(t, open) + // 1181/AC1: accepted with the provisioner client's JSON, and handed to the vault whole — the url and + // the fingerprint being exactly what issueProcess was given above, not a guess at what it computes. + taken := vault.taken["accept"] + for _, want := range []string{`"url":"nats://` + reachable + `"`, `"fingerprint":"ab"`, `"node":"laptop"`, + `"module":"mesh-mail"`, `"user":"laptop.mesh-mail.send"`, `"password"`} { + if !strings.Contains(taken, want) { + t.Errorf("the vault was handed %q, without %s", taken, want) + } + } + if !strings.Contains(vault.said(), `"name":"broker-send"`) { + t.Errorf("the vault was not told the secret's name broker-send: %s", vault.said()) + } + // The file's own owner (mesh-mail-send, review round 1 finding 7's area) is 1170's mechanism, proven + // generically there and for this fixture in TestAProcessesOwnSecretIsOwnedByItsOwnAccount. + + // 1181/AC2: issued again — the path `rollout mint`'s KindProcess case and a re-run of `module issue` + // both take — rotates the password; the old hash no longer matches what is recorded. issue() hash2, held, err := inv.BusUserHash(ctx, user) - if err != nil || !held || hash1 != hash2 { - t.Fatalf("the controller minted again with a vault installed (held %v, %v)", held, err) + if err != nil || !held { + t.Fatalf("the process's account is gone after a re-issue: held %v, %v", held, err) + } + if hash1 == hash2 { + t.Fatal("issuing the process's account again did not rotate its password") + } + + theGrantsPartTookOver(t, true) + issue() + if hash3, held, err := inv.BusUserHash(ctx, user); err != nil || !held || hash3 != hash2 { + t.Fatalf("the controller minted again after the grants part's take-over (held %v, %v)", held, err) } if said := vault.said(); !strings.Contains(said, `"verb":"issue-account"`) || !strings.Contains(said, `"user":"`+user+`"`) { - t.Errorf("the vault was not asked to issue %s: %s", user, said) + t.Errorf("the vault was not asked to issue %s after the take-over: %s", user, said) } } diff --git a/cmd/mesh-controller/reissue_verified_sender.go b/cmd/mesh-controller/reissue_verified_sender.go index 0ad71848..daeec95b 100644 --- a/cmd/mesh-controller/reissue_verified_sender.go +++ b/cmd/mesh-controller/reissue_verified_sender.go @@ -207,8 +207,13 @@ func giveVerifiedSendersBack(ctx context.Context, d *doctor) { } // reissueVerifiedSendersOn is reissueVerifiedSenders on the bus: memberships read from the AssignmentsStream and -// published to it. +// published to it. Once the stream is gone (the bus module's tools delete it at the lease write, research 072 row 33) +// the memberships are the grants part's alone, and there is nothing here to issue again. func reissueVerifiedSendersOn(ctx context.Context, bus link.OverNATS, records broker.Records, where broker.Placements) int { + if _, err := bus.JS.StreamInfo(broker.AssignmentsStream, nats.Context(ctx)); errors.Is(err, nats.ErrStreamNotFound) { + fmt.Printf(" the assignments stream is gone: the grants part issues the memberships, none is given a verified sender here\n") + return 0 + } return reissueVerifiedSenders(records, where, func(node, module string) ([]byte, uint64, error) { m, err := bus.JS.GetLastMsg(broker.AssignmentsStream, broker.MembershipSubject(node, module)) diff --git a/cmd/mesh-controller/reissue_verified_sender_integration_test.go b/cmd/mesh-controller/reissue_verified_sender_integration_test.go index 3dcc45d4..823fa87e 100644 --- a/cmd/mesh-controller/reissue_verified_sender_integration_test.go +++ b/cmd/mesh-controller/reissue_verified_sender_integration_test.go @@ -6,6 +6,8 @@ import ( "encoding/json" "testing" + "github.com/nats-io/nats.go" + "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" @@ -17,7 +19,10 @@ func TestNatsAMembershipIssuedWithoutItsVerifiedSenderIsIssuedAgainWhenTheJudgem t.Fatal(err) } t.Cleanup(js.Close) - if err := broker.AssertMeshStreams(js); err != nil { + // The assignments stream is no longer the mesh's (research 072 row 33): it is asserted by nothing, and stays on a + // bus only until the lease write deletes it. Made here as it is left there. + if _, err := js.Context().AddStream(&nats.StreamConfig{Name: broker.AssignmentsStream, + Subjects: []string{"mesh.assignment.*.*"}, MaxMsgsPerSubject: 1, AllowDirect: true}); err != nil { t.Fatal(err) } bus := link.OverNATS{Conn: js.Conn(), JS: js.Context()} diff --git a/go.mod b/go.mod index ab449f93..f29fb348 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-controller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 github.com/jackc/pgx/v5 v5.10.0 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/go.sum b/go.sum index 4a5eb8ad..0b60d1bd 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,6 @@ git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYg git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031083427-de8e82c06f54 h1:xITIabv8ujmwpaEKBiQpkOx6U/ICLTTzX9WWC3DGOPQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031083427-de8e82c06f54/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/broker/modstreams_live_test.go b/internal/broker/modstreams_live_test.go index 92e6f996..3478d88c 100644 --- a/internal/broker/modstreams_live_test.go +++ b/internal/broker/modstreams_live_test.go @@ -21,7 +21,13 @@ func TestAModuleStreamOnARealBus(t *testing.T) { t.Fatal(err) } defer js.Close() - for _, s := range []Stream{MeshStreams()[3], mailStream.Stream()} { + var events Stream + for _, s := range MeshStreams() { + if s.Name == EventsStream { + events = s + } + } + for _, s := range []Stream{events, mailStream.Stream()} { _ = js.js.DeleteStream(s.Name) if err := js.EnsureStream(s); err != nil { t.Fatalf("%s: %v", s.Name, err) diff --git a/internal/broker/stream_names_agreement_test.go b/internal/broker/stream_names_agreement_test.go index 9990f2d0..92876a25 100644 --- a/internal/broker/stream_names_agreement_test.go +++ b/internal/broker/stream_names_agreement_test.go @@ -14,7 +14,9 @@ import ( // queues are left on the bus until a cleanup removes them, and no module may take their names meanwhile. So the catalogue // reserves every name of the broker's own, and may reserve these retired ones beside them. func TestTheCataloguesReservedStreamNamesAreTheMeshsOwn(t *testing.T) { - retired := []string{"CHECKS_OTHER", "CHECKS_READY"} + // And ASSIGNMENTS (research 072 row 33): the bus module's tools delete it at the lease write, and the controller + // asserts it no more, so it is never made again. + retired := []string{"CHECKS_OTHER", "CHECKS_READY", "ASSIGNMENTS"} var own []string for _, s := range MeshStreams() { own = append(own, s.Name) diff --git a/internal/broker/streams.go b/internal/broker/streams.go index 052d47a8..14d6d49b 100644 --- a/internal/broker/streams.go +++ b/internal/broker/streams.go @@ -171,14 +171,9 @@ func MeshStreams() []Stream { Why: "one declaration per node, always the newest; a node that sees sequence n refuses " + "n-1 by construction (issue 107)", }, - { - Name: AssignmentsStream, - Subjects: []string{"mesh.assignment.*.*"}, - Retention: RetentionLastPerSubject, - Direct: true, - Why: "one membership per assignment, always the newest: what the mesh issued this module " + - "on this machine to serve and to reach (ADR 0160); read directly by the runtime it is for", - }, + // The assignments stream (ADR 0160) is no longer the mesh's: a runtime reads its membership from the grants + // part's state memberships (ADR 0571 clause 7), and the bus module's tools delete the stream at the lease write + // (research 072 row 33), so it is not declared here, which would make it again at the controller's next start. { Name: EventsStream, // A seat's own events ride here too: they are 1:many like any event, and the diff --git a/internal/broker/streams_test.go b/internal/broker/streams_test.go index 7a52d422..42b57243 100644 --- a/internal/broker/streams_test.go +++ b/internal/broker/streams_test.go @@ -123,10 +123,11 @@ func subjectMatches(filter, subject string) bool { // Each relationship's retention is the thing that makes it what it is (design 29 §4). func TestEachStreamCarriesTheRetentionItsShapeNeeds(t *testing.T) { want := map[string]Retention{ - "CONTROL": RetentionWorkQueue, - "NODES": RetentionLastPerSubject, - "EVENTS": RetentionLimits, - "ASSIGNMENTS": RetentionLastPerSubject, + "CONTROL": RetentionWorkQueue, + "NODES": RetentionLastPerSubject, + "EVENTS": RetentionLimits, + // No ASSIGNMENTS: the bus module's tools delete it at the lease write and nothing makes it again (research 072 + // row 33); a runtime reads its membership from the grants part's state. // What a consumer gave up on (novox/hq issue 330): the server's notice taken once, the message // kept until somebody acts. "DEAD_LETTER_NOTICES": RetentionWorkQueue, diff --git a/testdata/beside/CAPTURED b/testdata/beside/CAPTURED index 15ee0af4..9fb61a4d 100644 --- a/testdata/beside/CAPTURED +++ b/testdata/beside/CAPTURED @@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself. mesh-catalog f3a57dcc56fb6c198095cafd11d0decff9f720c4 modules/*/module.json, modules/*/files, modules/*/templates, predefined.json mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock -mesh-sdk acbf80ad910a3492dd9a970ac17f9b29579c4e56 conformance/events/module-event.json +mesh-sdk 2da0de75c3f0087cff9c2f5ef789ca6dc3c59045 conformance/events/module-event.json To move them, from this repository's root, with the two repositories checked out beside it: