package main import ( "context" "encoding/json" "log" "strings" "sync" "time" "github.com/novox/mesh-controller/internal/link" ) // A value given by hand lives only until the module's first good start (novox/hq ADR 0228). // // The serving controller hears every report; a clean one about the declaration a machine was last // sent is the signal the store asks about (inventory.ReplaceGivenAfterStart). What it replaced is // sent at once, said in the log and stated on the bus as the controller seat's `secret-replaced`, // so a replacement is never silent. **Not in the hand-act log**: nobody acted by hand, and that log // is read as the count of repairs a healer is wanted for. // SecretReplaced is the controller seat's fact that a value given by hand was replaced // (link.KeySecretReplaced). Never the value: neither the old one, which the mesh cannot read, // nor the new one, sealed to the machine as it was made. type SecretReplaced struct { Node string `json:"node"` Module string `json:"module"` Name string `json:"name"` Given time.Time `json:"given"` // Sent are the machines sent so the module starts again on the new value; Unsent says why // they could not be, in which case the next push carries it. Sent []string `json:"sent"` Unsent string `json:"unsent,omitempty"` Why string `json:"why"` } // givenEvents is where the serving controller states it; nil in a command. var givenEvents link.Bus // replacing keeps one replacement per machine at a time: two reports arriving together find the // same rows, and the store's claim makes one of them the replacer, but the sends need not race. var replacing sync.Map // startedWell says a report is a machine's clean account of a declaration: everything applied, // nothing failed or refused. Whether it is the declaration last sent is the store's to answer. func startedWell(report link.Report) bool { return report.Declared != "" && report.Refused == "" && len(report.Failed) == 0 && report.Applied != nil } const givenWhy = "a value given by hand lives only until its module's first good start under the mesh (novox/hq ADR 0228)" // replaceGiven replaces what the report makes due, sends the machines, and says so. func replaceGiven(ctx context.Context, open *stores, report link.Report) { if _, busy := replacing.LoadOrStore(report.Node, true); busy { return } defer replacing.Delete(report.Node) replaced, err := open.inventory.ReplaceGivenAfterStart(ctx, report.Node, report.Declared) if err != nil { log.Printf("a value given by hand on %s could not be replaced after its module started: %v", report.Node, err) } for _, r := range replaced { said := SecretReplaced{Node: report.Node, Module: r.Module, Name: r.Name, Given: r.Given.UTC(), Sent: r.Machines, Why: givenWhy} log.Printf("replaced %q of %s on %s, given %s, with a value the mesh made: %s; sending %s", r.Name, r.Module, report.Node, r.Given.UTC().Format(time.RFC3339), givenWhy, strings.Join(r.Machines, ", ")) if err := sendTo(ctx, open, r.Machines); err != nil { said.Sent, said.Unsent = nil, err.Error() log.Printf("the new %q of %s is sealed and not yet delivered to %s — the next push carries it: %v", r.Name, r.Module, strings.Join(r.Machines, ", "), err) } stateReplaced(ctx, said) } } func stateReplaced(ctx context.Context, said SecretReplaced) { if givenEvents == nil { return } body, err := json.Marshal(said) if err != nil { log.Printf("could not say that %s's %q was replaced: %v", said.Module, said.Name, err) return } stating, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := givenEvents.PublishSeatEvent(stating, link.MeshControllerSeat, link.KeySecretReplaced, body); err != nil { log.Printf("could not say that %s's %q was replaced: %v", said.Module, said.Name, err) } }