diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 0a12e7e..5b55123 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -648,14 +648,9 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus shadowHooks := hooks shadowHooks.refused, shadowHooks.certify = nil, nil shadowEnv := composerEnv(session, node, say, shadowHooks) - // And, after each answer, what runs of an own secret the answer says the vault holds another value of: handed to - // the vault only while a node applied the controller's compositions (issue 1126, cause C4; ownresync), so nothing now. - composes := &composesNow{} - resync := ownResync(session, node, say, shadowEnv, composes) shadow := selfcompose.NewShadow(shadowEnv, func(ctx context.Context, answers map[string]delivery.CandidateAnswer) { session.NoteCandidates(ctx, delivery.WithValuesDiffer(answers)) - go resync.take(ctx, answers) }) go shadow.Keep(run, link.Backoff) // this run's alone: a switch begins a run, and its shadow, again // Its own composer, always (issue 1126, A7): a composition made elsewhere (the controller's, in the node's @@ -664,7 +659,6 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus composer := selfComposer(session, node, hooks, say) machinesNow.Store(composer) // the machines a failing network part points at, by the placements (issue 774) composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone - composes.set(delivery.ComposesOwn) env.Composed, env.SelfComposed, env.Policy = composer, true, composer.Policy env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does session.NoteComposes(linked, delivery.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere") diff --git a/cmd/mesh-host/nodekey.go b/cmd/mesh-host/nodekey.go index 5881baf..5b1aa9e 100644 --- a/cmd/mesh-host/nodekey.go +++ b/cmd/mesh-host/nodekey.go @@ -26,12 +26,10 @@ const ( ) // heardKey is the vault's own public key as its answer to node-key gave it (ADR 0547 clause 9, issue 1945): the key the -// bus node hands the loaded list's hashes sealed to. One for the process, as the key stated is; kick is the follower of -// the user list on the link open now, told when the key is first heard or changes. +// bus node hands the loaded list's hashes sealed to. One for the process, as the key stated is. type heardKey struct { - mu sync.Mutex - pem string - kick func() + mu sync.Mutex + pem string } // vaultKey is the vault's key this engine heard. @@ -43,8 +41,7 @@ func (h *heardKey) get() string { return h.pem } -// heard keeps the key a node-key answer gave, and tells the follower when it is a first or changed one. An answer -// with no key (a vault before this one) changes nothing. +// heard keeps the key a node-key answer gave. An answer with no key (a vault before this one) changes nothing. func (h *heardKey) heard(answer json.RawMessage) { var a struct { VaultKey string `json:"vault_key"` @@ -53,24 +50,8 @@ func (h *heardKey) heard(answer json.RawMessage) { return } h.mu.Lock() - changed := a.VaultKey != h.pem h.pem = a.VaultKey - kick := h.kick h.mu.Unlock() - if changed && kick != nil { - kick() - } -} - -// following names the follower told when the key changes, and tells it at once when a key is known. -func (h *heardKey) following(kick func()) { - h.mu.Lock() - h.kick = kick - known := h.pem != "" - h.mu.Unlock() - if known && kick != nil { - kick() - } } // nodeKeyReask is how often the key is stated again though it did not change, so a changed vault key is heard: the diff --git a/cmd/mesh-host/nodekey_test.go b/cmd/mesh-host/nodekey_test.go index 0bae9f5..6084ced 100644 --- a/cmd/mesh-host/nodekey_test.go +++ b/cmd/mesh-host/nodekey_test.go @@ -162,29 +162,22 @@ func asksAgainUntilItHearsTheVaultsKey(t *testing.T) { } // The statement a host makes names this contract (issue 786), one above the vault's box. And the vault's key its -// answer gives is kept, told to the follower once when first heard and again only when changed; an answer without one -// (an older vault) or with no public key changes nothing (ADR 0547 clause 9). +// answer gives is kept; an answer without one (an older vault) or with no public key changes nothing (ADR 0547 clause +// 9). func TestTheNodeKeyContractIsThirteen(t *testing.T) { if link.NodeKeyContract != 13 { t.Errorf("NodeKeyContract %d", link.NodeKeyContract) } h := &heardKey{} - kicks := 0 - h.following(func() { kicks++ }) h.heard(json.RawMessage(`{"recorded":true}`)) h.heard(json.RawMessage(`{"recorded":true,"vault_key":"not a key"}`)) - if h.get() != "" || kicks != 0 { - t.Fatalf("an answer with no key was kept: %q, %d kicks", h.get(), kicks) + if h.get() != "" { + t.Fatalf("an answer with no key was kept: %q", h.get()) } pem := "-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----\n" answer, _ := json.Marshal(map[string]any{"recorded": true, "vault_key": pem}) h.heard(answer) - h.heard(answer) - if h.get() != pem || kicks != 1 { - t.Fatalf("key %q, %d kicks, want the key and one", h.get(), kicks) - } - h.following(func() { kicks += 10 }) - if kicks != 11 { - t.Fatalf("a follower named after the key was heard was not told: %d", kicks) + if h.get() != pem { + t.Fatalf("key %q, want the key", h.get()) } } diff --git a/cmd/mesh-host/ownresync.go b/cmd/mesh-host/ownresync.go deleted file mode 100644 index 21d254e..0000000 --- a/cmd/mesh-host/ownresync.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "sync" - "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - - "github.com/novox/mesh-host/internal/identity" - "github.com/novox/mesh-host/internal/link" - "github.com/novox/mesh-host/internal/ownresync" - "github.com/novox/mesh-host/internal/selfcompose" -) - -// composesNow is whose compositions this run of the agent applies, once decided ("" before). -type composesNow struct { - mu sync.Mutex - now string -} - -func (c *composesNow) set(s string) { c.mu.Lock(); c.now = s; c.mu.Unlock() } -func (c *composesNow) get() string { c.mu.Lock(); defer c.mu.Unlock(); return c.now } - -// resyncStep is the step that hands the vault what runs of an own secret a shadow answer says another value of (issue -// 1126, cause C4; internal/ownresync): one pass at a time, and the last answers taken again once the agent has decided -// whose compositions it applies. -type resyncStep struct { - r *ownresync.Resync - one sync.Mutex - mu sync.Mutex - last map[string]delivery.CandidateAnswer - // tries and pending are the step taken again after a bound when a read or an ask the bus did not answer stopped it - // (Outcome.Retry, issue 1126, 2026-10-28), one wait at a time. - tries int - pending bool -} - -func ownResync(session *link.Session, node string, say func(string), env selfcompose.Env, composes *composesNow) *resyncStep { - return &resyncStep{r: ownresync.New(ownresync.Env{Node: node, Composes: composes.get, Running: env.Running, - Get: env.Get, Unseal: env.Unseal, Seal: identity.Seal, Say: say, - Ask: func(ctx context.Context, verb string, args map[string]any) (json.RawMessage, error) { - return session.AskVault(ctx, verb, args) - }})} -} - -// take keeps answers as the last and takes them. -func (s *resyncStep) take(ctx context.Context, answers map[string]delivery.CandidateAnswer) { - s.mu.Lock() - s.last = answers - s.mu.Unlock() - s.again(ctx) -} - -// again takes the last answers, and once more after a bound (selfcompose.LaterWait) while the bus did not answer for one. -func (s *resyncStep) again(ctx context.Context) { - s.one.Lock() - defer s.one.Unlock() - s.mu.Lock() - last := s.last - s.mu.Unlock() - if last == nil { - return - } - retry := false - for _, o := range s.r.Take(ctx, last) { - retry = retry || o.Retry - } - s.mu.Lock() - defer s.mu.Unlock() - if !retry { - s.tries = 0 - return - } - if s.pending || ctx.Err() != nil { - return - } - wait := selfcompose.LaterWait(s.tries) - s.tries++ - s.pending = true - go func() { - select { - case <-ctx.Done(): - case <-time.After(wait): - } - s.mu.Lock() - s.pending = false - s.mu.Unlock() - if ctx.Err() == nil { - s.again(ctx) - } - }() -} diff --git a/internal/ownresync/ownresync.go b/internal/ownresync/ownresync.go deleted file mode 100644 index dbae480..0000000 --- a/internal/ownresync/ownresync.go +++ /dev/null @@ -1,319 +0,0 @@ -// Package ownresync hands the vault the value of this machine's own secret that runs, where the vault holds another -// (novox/hq issue 1126, cause C4). -// -// The vault's `move-in` never replaces a value it holds, so its first import of an own secret stays fixed; where the -// controller later made that secret anew (a rotation at start, a re-issue), the new value runs and the vault keeps the -// old one. The node's shadow composer sees it: composing from the vault's value, it opens both seals with this -// machine's key and its answer's shown body says delivery.MaskedAnotherValue. After each shadow answer this step asks the -// vault's `move-in` with replace_running: true and what runs, for each own secret so masked, once per running value, and -// says what it asked and what the vault answered. It is move-in's, the verb this node may ask already, so no seat and no -// bus grant changes for it. -// -// **Only while this node applies the controller's compositions**, and only for a module whose composition that runs is -// the one its want names: then the controller's value is what runs, and what runs is the truth the vault lacks. Once the -// node composes its own, the vault's value is the source and nothing is handed. **The switch** is the vault's -// (`own_take_switch`, a person's act): while it is off the vault refuses every ask, and this step says so once per value. -// The vault also refuses any value it changed in the last 15 minutes, so a re-issue the node has not applied yet is -// never put back. -package ownresync - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "sort" - "sync" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" - - "github.com/novox/mesh-host/internal/selfcompose" -) - -// Verb is the vault's verb this step asks, with ReplaceRunning true in both steps. -const Verb = "move-in" - -// ReplaceRunning is move-in's argument that makes it take the running value over the one the vault holds. -const ReplaceRunning = "replace_running" - -// Env is what the step reads and asks. -type Env struct { - Node string - // Composes is whose compositions the node applies now: delivery.ComposesController, delivery.ComposesOwn, or "" - // while its agent has not decided. The step hands nothing but for ComposesController. - Composes func() string - // Running is what the node runs now (agent.Everything). - Running func() []delivery.Composed - // Get reads one key's latest value; nil and no error when there is none. - Get func(ctx context.Context, bucket, key string) ([]byte, error) - // Unseal opens a value sealed to this machine; Seal seals a value to a raw X25519 key in base64 (identity.Seal). - Unseal func(sealed string) ([]byte, error) - Seal func(to string, value []byte) (string, error) - // Ask asks one of the vault's verbs in this engine's own name (link.Session.AskVault). - Ask func(ctx context.Context, verb string, args map[string]any) (json.RawMessage, error) - Say func(string) -} - -// Outcome is what the step did for one own secret: asked or not, and what was said. -type Outcome struct { - Module string - Name string - Asked bool - Said string - // Retry says a read or an ask the bus did not answer stopped it: no answer, kept as none, and the step is to be - // taken again after a bound (issue 1126, 2026-10-28: "want.shanks.brother could not be read ... no responders"). - Retry bool -} - -// Resync is the step of one node agent's run. -type Resync struct { - env Env - - mu sync.Mutex - // done is, by module/name, the running value's fingerprint the vault was asked with or why it was not: each is - // asked, and each reason said, once. - done map[string]string -} - -// New is the step for one node. -func New(env Env) *Resync { - if env.Say == nil { - env.Say = func(string) {} - } - return &Resync{env: env, done: map[string]string{}} -} - -// Take reads the node's shadow answers and hands the vault what runs of each own secret they say another value of. -func (r *Resync) Take(ctx context.Context, answers map[string]delivery.CandidateAnswer) []Outcome { - seen := map[delivery.OwnValueDiffers]bool{} - var drift []delivery.OwnValueDiffers - proposals := make([]string, 0, len(answers)) - for g := range answers { - proposals = append(proposals, g) - } - sort.Strings(proposals) - for _, g := range proposals { - for _, o := range delivery.OwnValuesDiffer(answers[g]) { - if !seen[o] { - seen[o] = true - drift = append(drift, o) - } - } - } - if len(drift) == 0 { - return nil - } - composes := "" - if r.env.Composes != nil { - composes = r.env.Composes() - } - var out []Outcome - for _, o := range drift { - res := r.one(ctx, composes, o) - if res.Said != "" || res.Retry { - out = append(out, res) - } - } - return out -} - -// one is the step for one own secret: Said is empty when it was done before for the same value and reason. -func (r *Resync) one(ctx context.Context, composes string, o delivery.OwnValueDiffers) Outcome { - res := Outcome{Module: o.Module, Name: o.Name} - key := o.Module + "/" + o.Name - if composes == "" { - return res // the agent has not decided whose compositions it applies: taken again once it has - } - if composes != delivery.ComposesController { - return r.said(key, "not:composes", res, fmt.Sprintf("%s holds another value in the vault than runs here, and "+ - "nothing is handed: this node composes its own (%q), so the vault's value is the source", key, composes)) - } - sealed, why, failed := r.running(ctx, o) - if failed != nil { - // The bus did not answer: no reason to keep, and taken again (Retry); said once per reason. - res.Retry = true - return r.said(key, "retry:"+why, res, fmt.Sprintf("%s holds another value in the vault than runs here, and "+ - "nothing is handed yet: %s; asked again", key, why)) - } - if why != "" { - return r.said(key, "not:"+why, res, fmt.Sprintf("%s holds another value in the vault than runs here, and "+ - "nothing is handed: %s", key, why)) - } - sum := sha256.Sum256([]byte(sealed)) - fp := hex.EncodeToString(sum[:]) - r.mu.Lock() - asked := r.done[key] == fp - r.mu.Unlock() - if asked { - return res - } - value, err := r.env.Unseal(sealed) - if err != nil { - return r.said(key, "not:unseal:"+fp, res, fmt.Sprintf("%s: what runs does not open with this machine's key, so "+ - "nothing is handed: %v", key, err)) - } - defer wipe(value) - said, err := r.hand(ctx, o, value) - if err != nil { - if ctx.Err() != nil { - return res // the run ended: asked again in the next - } - if selfcompose.BusFailed(err) { - // The vault did not answer on the bus: no word of the vault's, so not kept as asked (Retry). - res.Retry = true - return r.said(key, "retry:ask", res, fmt.Sprintf("%s: the vault's %s with replace_running did not answer, "+ - "and is asked again: %v", key, Verb, err)) - } - said = fmt.Sprintf("%s: the vault's %s with replace_running refused: %v", key, Verb, err) - } - // Once per running value, whatever the vault answered: a refusal is the vault's word, said once, and a new value - // that runs asks again. - r.mu.Lock() - r.done[key] = fp - r.mu.Unlock() - res.Asked, res.Said = true, said - r.env.Say(said) - return res -} - -// said says why once per reason, and keeps the reason as done. -func (r *Resync) said(key, reason string, res Outcome, why string) Outcome { - r.mu.Lock() - again := r.done[key] == reason - r.done[key] = reason - r.mu.Unlock() - if again { - return res - } - res.Said = why - r.env.Say(why) - return res -} - -// running is the sealed value of o in the composition that runs, when that composition is the one the module's want -// names; or why it is not handed, with the error of a read the bus did not answer, which is no answer. -func (r *Resync) running(ctx context.Context, o delivery.OwnValueDiffers) (sealed, why string, failed error) { - var comp *delivery.Composed - if r.env.Running != nil { - for _, c := range r.env.Running() { - if c.Module == o.Module { - c := c - comp = &c - break - } - } - } - if comp == nil { - return "", o.Module + " runs no composition here", nil - } - wantKey, err := delivery.WantKey(r.env.Node, o.Module) - if err != nil { - return "", err.Error(), nil - } - raw, err := r.env.Get(ctx, delivery.AssignerWantsBucket, wantKey) - if err != nil { - return "", fmt.Sprintf("its want %s could not be read (%v)", wantKey, err), err - } - if raw == nil { - return "", fmt.Sprintf("it has no want %s", wantKey), nil - } - want, err := delivery.DecodeWant(raw) - if err != nil { - return "", fmt.Sprintf("its want %s is not one: %v", wantKey, err), nil - } - if want.Self || want.Removed || want.Composed == "" { - return "", fmt.Sprintf("its want %s names no composition of the controller's", wantKey), nil - } - // What runs is the composition the want names, as the module's have says: applied and running. - haveKey, err := delivery.HaveKey(r.env.Node, o.Module) - if err != nil { - return "", err.Error(), nil - } - raw, err = r.env.Get(ctx, delivery.HaveBucket, haveKey) - if err != nil { - return "", fmt.Sprintf("its have %s could not be read (%v)", haveKey, err), err - } - if raw == nil { - return "", fmt.Sprintf("it has no have %s", haveKey), nil - } - have, err := delivery.DecodeHave(raw) - if err != nil { - return "", fmt.Sprintf("its have %s is not one: %v", haveKey, err), nil - } - if have.Composed != want.Composed || have.Phase != delivery.PhaseRunning { - return "", fmt.Sprintf("what runs (%s, %s) is not yet the composition its want names (%s)", have.Composed, - have.Phase, want.Composed), nil - } - // On a node the resource's id carries its module's name (`only-office.needs-jwt`); delivery.OwnNeedName reads it. - id := o.Module + "." + catalogue.NeedID(o.Name) - for _, res := range comp.Resources { - var body struct { - ID string `json:"id"` - Sealed any `json:"sealed"` - } - if json.Unmarshal(res, &body) != nil { - continue - } - if name, own := delivery.OwnNeedName(o.Module, body.ID); !own || name != o.Name { - continue - } - if s, ok := body.Sealed.(string); ok && s != "" { - return s, "", nil - } - return "", fmt.Sprintf("its resource %s carries no value sealed to this machine", id), nil - } - return "", fmt.Sprintf("what runs holds no resource %s", id), nil -} - -// hand asks the vault's two steps with value, and checks what the vault holds after by its fingerprint. -func (r *Resync) hand(ctx context.Context, o delivery.OwnValueDiffers, value []byte) (string, error) { - raw, err := r.env.Ask(ctx, Verb, map[string]any{ReplaceRunning: true, "module": o.Module, "name": o.Name, - "why": "this node's shadow answer says the vault holds another value than runs here"}) - if err != nil { - return "", err - } - var opened struct { - Call string `json:"call"` - SealTo string `json:"seal_to"` - Secret string `json:"secret"` - } - if err := json.Unmarshal(raw, &opened); err != nil || opened.Call == "" || opened.SealTo == "" { - return "", fmt.Errorf("the vault answered no call and no key to seal to") - } - sealed, err := r.env.Seal(opened.SealTo, value) - if err != nil { - return "", fmt.Errorf("the vault's key for the call is not a sealing key: %w", err) - } - raw, err = r.env.Ask(ctx, Verb, map[string]any{ReplaceRunning: true, "call": opened.Call, "sealed": sealed}) - if err != nil { - return "", err - } - var kept struct { - Secret string `json:"secret"` - From int `json:"from_generation"` - To int `json:"to_generation"` - Unchanged bool `json:"unchanged"` - Fingerprint string `json:"fingerprint"` - Said string `json:"said"` - } - if err := json.Unmarshal(raw, &kept); err != nil || kept.Secret == "" { - return "", fmt.Errorf("the vault answered %s with no secret", opened.Secret) - } - sum := sha256.Sum256(value) - if kept.Fingerprint != "sha256:"+hex.EncodeToString(sum[:]) { - return "", fmt.Errorf("the vault says it holds %s at generation %d, but not the value handed in", kept.Secret, kept.To) - } - if kept.Unchanged { - return fmt.Sprintf("handed the vault what runs of %s: it held that value already (generation %d)", kept.Secret, kept.To), nil - } - return fmt.Sprintf("handed the vault what runs of %s: generation %d replaced by %d, the value that runs", kept.Secret, - kept.From, kept.To), nil -} - -func wipe(b []byte) { - for i := range b { - b[i] = 0 - } -} diff --git a/internal/ownresync/ownresync_test.go b/internal/ownresync/ownresync_test.go deleted file mode 100644 index d4351c2..0000000 --- a/internal/ownresync/ownresync_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package ownresync - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// Issue 1126, cause C4: after a shadow answer saying the vault holds another value of an own secret than runs, the node -// hands the vault what runs, in the verb's two steps, once per running value; and only while it applies the -// controller's compositions and runs the one its want names. -func TestTheNodeHandsTheVaultWhatRunsOfADriftedOwnSecret(t *testing.T) { - const running = "the-value-that-runs" - digest := "sha256:" + strings.Repeat("a", 64) - fp := "src2:" + strings.Repeat("b", 64) - sum := sha256.Sum256([]byte(running)) - answers := map[string]delivery.CandidateAnswer{"g": {Proposal: "p-1", Revision: 1, Verdict: delivery.CandidateDiffers, - Differs: []delivery.ModuleDiffers{{Module: "only-office", Why: "the resource differs", Bodies: []delivery.ResourceBodies{ - {ID: "only-office.needs-jwt", Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}, - {ID: "only-office.needs-salt", Composed: `{"sealed":""}`}}}}}} - for name, row := range map[string]struct { - composes string - runs string // the have's composition digest - refuse string // the vault's refusal - asks int // the vault's verb asked, both passes - says string - // Issue 1126, 2026-10-28: a read or an ask the bus did not answer is no answer: taken again (Retry), never kept. - wantRead string // the want's read: "" answers it, "fails" the bus does not answer, "absent" no key - retry bool - }{ - "applies the controller's, runs its want": {delivery.ComposesController, digest, "", 2, "generation 1 replaced by 2", "", false}, - "composes its own": {delivery.ComposesOwn, digest, "", 0, "composes its own", "", false}, - "not decided yet": {"", digest, "", 0, "", "", false}, - "runs not yet what its want names": {delivery.ComposesController, "sha256:" + strings.Repeat("c", 64), "", 0, "not yet the composition", "", false}, - "the vault refuses": {delivery.ComposesController, digest, "switched off", 1, "switched off", "", false}, - "the vault does not answer": {delivery.ComposesController, digest, "nats: no responders available for request", 2, "asked again", "", true}, - "the want's read does not answer": {delivery.ComposesController, digest, "", 0, "asked again", "fails", true}, - "it has no want": {delivery.ComposesController, digest, "", 0, "it has no want", "absent", false}, - } { - var asked []map[string]any - var said []string - want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: digest}) - have, _ := delivery.EncodeHave(delivery.Have{FP: fp, Composed: row.runs, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) - r := New(Env{Node: "novox", Composes: func() string { return row.composes }, - Running: func() []delivery.Composed { - return []delivery.Composed{{Node: "novox", Module: "only-office", FP: fp, Resources: []json.RawMessage{ - // As novox runs it (its shadow answer, 2026-10-28): the id carries the module's name. - json.RawMessage(`{"id":"only-office.needs-jwt","module":"only-office","type":"file",` + - `"path":"/var/lib/only-office/jwt.secret","sealed":"box-of-the-running-value"}`)}}} - }, - Get: func(_ context.Context, bucket, key string) ([]byte, error) { - switch { - case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office" && row.wantRead == "fails": - return nil, errors.New("the bucket mesh-delivery-assigner_wants: nats: no responders available for request") - case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office" && row.wantRead == "absent": - return nil, nil - case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office": - return want, nil - case bucket == delivery.HaveBucket && key == "novox.only-office": - return have, nil - } - return nil, nil - }, - Unseal: func(sealed string) ([]byte, error) { - if sealed != "box-of-the-running-value" { - return nil, errors.New("not this machine's") - } - return []byte(running), nil - }, - Seal: func(to string, value []byte) (string, error) { return to + "(" + string(value) + ")", nil }, - Ask: func(_ context.Context, verb string, args map[string]any) (json.RawMessage, error) { - asked = append(asked, args) - if verb != "move-in" || args["replace_running"] != true { - t.Errorf("%s: asked %s with %v, not move-in with replace_running", name, verb, args) - } - if row.refuse != "" { - return nil, errors.New(row.refuse) - } - if args["call"] == nil { - if args["module"] != "only-office" || args["name"] != "jwt" || args["node"] != nil { - t.Errorf("%s: the first step names %v", name, args) - } - return json.RawMessage(`{"call":"c1","seal_to":"key","secret":"own/novox/only-office/jwt"}`), nil - } - if args["sealed"] != "key("+running+")" { - t.Errorf("%s: handed %v, not what runs sealed to the call's key", name, args["sealed"]) - } - return json.RawMessage(`{"secret":"own/novox/only-office/jwt","from_generation":1,"to_generation":2,` + - `"fingerprint":"sha256:` + hex.EncodeToString(sum[:]) + `"}`), nil - }, - Say: func(s string) { said = append(said, s) }}) - first := r.Take(context.Background(), answers) - again := r.Take(context.Background(), answers) - if len(asked) != row.asks { - t.Errorf("%s: the vault asked %d times, want %d: %v", name, len(asked), row.asks, asked) - } - if retry := len(first) == 1 && first[0].Retry; retry != row.retry { - t.Errorf("%s: taken again %v, want %v: %+v", name, retry, row.retry, first) - } - if row.retry && (len(again) != 1 || !again[0].Retry || again[0].Said != "") || !row.retry && len(again) != 0 { - t.Errorf("%s: the same answer did or said something again: %+v", name, again) - } - all := strings.Join(said, "\n") - if row.says == "" && (len(first) != 0 || all != "") || row.says != "" && (len(first) != 1 || !strings.Contains(all, row.says)) { - t.Errorf("%s: did %+v and said %q, want %q", name, first, all, row.says) - } - if strings.Contains(all, running) { - t.Errorf("%s: the value is in what the node said", name) - } - } -} diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 802b4d4..d14cde2 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -1057,7 +1057,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { go func() { sh.Keep(sctx, func(int) time.Duration { return 5 * time.Millisecond }); close(kept) }() time.Sleep(30 * time.Millisecond) sb.missing(delivery.PlacementCandidatesBucket, false) - sb.made(7) + sb.made(6) rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) tp, _ := delivery.EncodePlacement(trunk) sb.put(delivery.PlacementBucket, "placement.n1", tp, 1) @@ -1321,7 +1321,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { nctx, ncancel := context.WithCancel(ctx) defer ncancel() go func() { _ = nsh.Run(nctx) }() - nb.made(7) + nb.made(6) nb.put(delivery.PlacementBucket, "placement.n1", tp, 1) nb.put(delivery.HaveBucket, "n1.node", rec, 1) for name, m := range mods { @@ -1335,19 +1335,19 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { oldRaw, _ := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "older", Placement: &trunk}) oldKey, _ := delivery.PlacementCandidateKey("older", "n1") nb.put(delivery.PlacementCandidatesBucket, oldKey, oldRaw, 2) - for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, delivery.BuilderBuildsBucket} { nb.send(bucket, agent.Update{Caught: true}) } select { case a := <-nanswered: - if g := a["same"]; g.Verdict != delivery.CandidateRefused || !strings.Contains(g.Refused, delivery.AssignerWantsBucket) || g.Proposal != "same" { - t.Errorf("a shadow waiting for its wants answered %+v", g) + if g := a["same"]; g.Verdict != delivery.CandidateRefused || !strings.Contains(g.Refused, ResolvedBucket) || g.Proposal != "same" { + t.Errorf("a shadow waiting for its values answered %+v", g) } case <-time.After(5 * time.Second): t.Fatal("a shadow not ready after its wait answered nothing") } nmu.Lock() - if !strings.Contains(strings.Join(nsaid, "\n"), "this node answers no placement candidate yet: the node has not read "+delivery.AssignerWantsBucket) { + if !strings.Contains(strings.Join(nsaid, "\n"), "this node answers no placement candidate yet: the node has not read "+ResolvedBucket) { t.Errorf("a shadow not ready said %q", nsaid) } nmu.Unlock() @@ -1355,7 +1355,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { scancel() <-kept setFor(t, &slowAfter, time.Nanosecond) // every answer is slow: said, and noted in words that do not move with the time - nb.send(delivery.AssignerWantsBucket, agent.Update{Caught: true}) + nb.send(ResolvedBucket, agent.Update{Caught: true}) select { case a := <-nanswered: if g := a["same"]; g.Verdict != delivery.CandidateSame || !strings.Contains(g.Note, "this answer took over") { diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 254699f..bf6c878 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -32,8 +32,6 @@ type Shadow struct { mu sync.Mutex candidates map[string]map[string]candidateKey // by proposal, by node answers map[string]delivery.CandidateAnswer - // wants are the node's wants by module (agent.NodeModule for its own); a change of one answers again. - wants map[string]delivery.Want // ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy // from the declaration kept before the switch (issue 1126, probe #532 on shanks). ran map[string]string @@ -64,7 +62,7 @@ type answeredAt struct { } // inputWords name the shadow's inputs, in Run's order, as an answer says which of them woke it. -var inputWords = [...]string{"the candidate", "placement", "records", "modules", "values", "haves", "wants"} +var inputWords = [...]string{"the candidate", "placement", "records", "modules", "values", "haves"} // wokenBy says what moved between the answer a proposal stands at (had, when done) and the one about to be composed (at): // its first answer, a retry, its candidate's revisions, or the inputs that changed, by their words. @@ -99,7 +97,7 @@ type candidateKey struct { // NewShadow is the shadow composer of one node; answer writes the node's answers into its own have key. func NewShadow(env Env, answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)) *Shadow { return &Shadow{c: New(env), answer: answer, candidates: map[string]map[string]candidateKey{}, - answers: map[string]delivery.CandidateAnswer{}, wants: map[string]delivery.Want{}, ran: map[string]string{}} + answers: map[string]delivery.CandidateAnswer{}, ran: map[string]string{}} } // Keep runs the shadow until ctx ends, beginning again after backoff(try) whenever it stops: an input it cannot watch @@ -141,7 +139,6 @@ func (s *Shadow) Run(ctx context.Context) error { defer cancel() s.mu.Lock() s.candidates = map[string]map[string]candidateKey{} - s.wants = map[string]delivery.Want{} s.ran = map[string]string{} s.answered = map[string]answeredAt{} s.moved = [len(inputWords)]uint64{} @@ -159,8 +156,6 @@ func (s *Shadow) Run(ctx context.Context) error { {ResolvedBucket, node + ".>", (*Composer).takeValue, nil}, // What this node runs moved: an apply ends in its module's have. {delivery.HaveBucket, node + ".>", nil, nil}, - // Its wants. - {delivery.AssignerWantsBucket, "want." + node + ".>", nil, nil}, } type tagged struct { in int @@ -225,8 +220,6 @@ func (s *Shadow) Run(ctx context.Context) error { case t.in == 5: folded := c.takeModuleHave(t.u) changed = s.takeRan(t.u) || folded - case t.in == 6: - changed = s.takeWant(t.u) default: changed = c.takeChanged(ins[t.in], t.u) } @@ -685,33 +678,6 @@ func sameAnswer(a, b delivery.CandidateAnswer) bool { return errA == nil && errB == nil && string(x) == string(y) } -// takeWant takes one of the node's wants, and says whether it changed. -func (s *Shadow) takeWant(u agent.Update) bool { - module := "" - var w delivery.Want - if _, err := delivery.ParseWantNodeKey(u.Key); err == nil { - module = agent.NodeModule - } else if node, m, err := delivery.ParseWantKey(u.Key); err == nil && node == s.c.env.Node { - module = m - if !u.Deleted { - if w, err = delivery.DecodeWant(u.Value); err != nil || w.Removed || w.FP == "" { - u.Deleted = true - } - } - } else { - return false - } - s.mu.Lock() - defer s.mu.Unlock() - old, had := s.wants[module] - if u.Deleted { - delete(s.wants, module) - return had - } - s.wants[module] = w - return !had || old.FP != w.FP -} - // sameValue says whether two sealed values open, with this node's key, to the same value; unknown when either does not // open (or the node holds no opener). Only the comparison leaves: never a value (issue 1126, the shadow shows its work). func (c *Composer) sameValue(a, b string) (same, known bool) {