Files
mesh-sdk/go/delivery/placement_test.go
T

574 lines
32 KiB
Go

package delivery
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// ADR 0549 §1, §2: the placement key and file, the value's refusals, its reading as the composition reads a node, what
// a change says, and the catalogue's check of every file.
func TestPlacement(t *testing.T) {
t.Run("key and file", func(t *testing.T) {
key, err := PlacementKey("g14")
if err != nil || key != "placement.g14" {
t.Fatalf("PlacementKey = %q, %v", key, err)
}
if n, err := ParsePlacementKey(key); err != nil || n != "g14" {
t.Fatalf("ParsePlacementKey = %q, %v", n, err)
}
for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b"} {
if _, err := ParsePlacementKey(bad); err == nil {
t.Errorf("ParsePlacementKey(%q) took it", bad)
}
}
if _, err := PlacementKey("a.b"); err == nil {
t.Error("PlacementKey took a dotted node")
}
// ADR 0567 §1: the seat's three states are named after the holding module, and every reader takes the names here.
for got, want := range map[string]string{PlacementBucket: "mesh-delivery-assigner_placement",
PlacementProposalsBucket: "mesh-delivery-assigner_proposals", PlacementCandidatesBucket: "mesh-delivery-assigner_candidates",
PlacementRead: "mesh-delivery-assigner.placement", PlacementCandidatesRead: "mesh-delivery-assigner.candidates",
PlacementProposalsRead: "mesh-delivery-assigner.proposals"} {
if got != want {
t.Errorf("a bucket is named %q, want %q", got, want)
}
}
if PlacementHistory != 64 || PlacementStreamDays != 30 {
t.Errorf("history %d, stream %d days", PlacementHistory, PlacementStreamDays)
}
})
g14 := Placement{Node: "g14", Revision: "abc",
Decided: NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}},
Modules: []PlacedModule{
{Module: "zsh"},
{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "novox", Module: "postgres"}}},
},
Pins: map[string]Provider{"shell": {Node: "g14", Module: "zsh"}},
Holds: []Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
}
novox := Placement{Node: "novox", Decided: NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []PlacedModule{{Module: "postgres", Ports: map[string]int{"5432": 5432}}},
Holds: []Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}}
t.Run("value", func(t *testing.T) {
raw, err := EncodePlacement(g14)
if err != nil {
t.Fatal(err)
}
back, err := DecodePlacement(raw)
if err != nil || back.V != Version || !SamePlacement(back, g14) {
t.Fatalf("round trip: %+v, %v", back, err)
}
moved := g14
moved.Revision = "def"
if !SamePlacement(g14, moved) {
t.Error("a new revision alone counts as a change")
}
if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("version 9: %v", err)
}
for name, bad := range map[string]string{
"module twice": `{"v":1,"node":"g14","modules":[{"module":"a"},{"module":"a"}]}`,
"port not number": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"http":80}}]}`,
"host port zero": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"80":0}}]}`,
"binding no node": `{"v":1,"node":"g14","modules":[{"module":"a","bindings":{"pg":{"module":"pg"}}}]}`,
"pin no module": `{"v":1,"node":"g14","modules":[],"pins":{"pg":{"node":"x"}}}`,
"hold not placed": `{"v":1,"node":"g14","modules":[],"holds":[{"claim":"s","module":"a"}]}`,
"node not a name": `{"v":1,"node":"g.14","modules":[]}`,
"value not shaped": `{"v":1,"node":"g14","modules":{}}`,
} {
if _, err := DecodePlacement([]byte(bad)); err == nil {
t.Errorf("%s: taken", name)
}
}
})
t.Run("as the composition reads it", func(t *testing.T) {
if got := g14.Assigned(); !reflect.DeepEqual(got, []string{"zsh", "app"}) {
t.Errorf("Assigned = %v, want the file's order", got)
}
if got := g14.ChosenPins(); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) {
t.Errorf("ChosenPins = %v", got)
}
if got := g14.Bound(); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) {
t.Errorf("Bound = %v", got)
}
if got := g14.HostPorts(); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) {
t.Errorf("HostPorts = %v", got)
}
if got := g14.Held(); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) {
t.Errorf("Held = %v", got)
}
layered := PlacedModule{Module: "app", HasMesh: true, Layer: map[string]any{"expose": map[string]any{"80": "mesh"}}}
want := []catalogue.Layer{
{From: catalogue.MeshWideLayer, Values: map[string]any{}},
{From: "g14", Values: map[string]any{"mode": "x"}, Placement: map[string]any{"expose": map[string]any{"80": "mesh"}}},
}
if got := layered.Layers("g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) {
t.Errorf("Layers = %#v, want %#v", got, want)
}
if got := (PlacedModule{Module: "a"}).Layers("g14", map[string]any{"k": 1}); len(got) != 1 || got[0].From != "g14" {
t.Errorf("values alone make the node's layer: %#v", got)
}
if got := (PlacedModule{Module: "a", HasLayer: true}).Layers("g14", nil); len(got) != 1 || got[0].Values == nil {
t.Errorf("an empty layer of the node's own is kept, with empty values: %#v", got)
}
if got := (PlacedModule{Module: "a"}).Layers("g14", nil); got != nil {
t.Errorf("no layer and no value is no layer: %#v", got)
}
empty := Placement{Node: "x"}
if empty.ChosenPins() != nil || empty.Bound() != nil || empty.HostPorts() != nil || len(empty.Held()) != 0 {
t.Error("an empty placement reads as something")
}
})
t.Run("what a change says", func(t *testing.T) {
after := g14
after.Modules = []PlacedModule{{Module: "app", Ports: map[string]int{"8080": 18081}}, {Module: "new"}}
got := ChangeOf(g14, after)
want := PlacementChange{Node: "g14", Added: []string{"new"}, Removed: []string{"zsh"}, Changed: []string{"app"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("ChangeOf = %+v, want %+v", got, want)
}
pinned := g14
pinned.Pins = nil
if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) {
t.Errorf("a pin change changes every module on the node: %+v", got)
}
if got := ChangeOf(Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
t.Errorf("a new node: %+v", got)
}
})
t.Run("the catalogue's check", func(t *testing.T) {
held := func(m string) bool { return m != "ghost" }
enrolled := func(n string) bool { return n != "stranger" }
if r := CheckPlacements(map[string]Placement{"g14": g14, "novox": novox}, held, enrolled); len(r) != 0 {
t.Fatalf("a sound placement is refused: %v", r)
}
// A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there.
for seat, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} {
a := Placement{Node: "ace", Modules: []PlacedModule{{Module: "dns"}}, Holds: []Holding{{Claim: seat, Scope: "mesh", Module: "dns"}}}
b := a
b.Node = "novox"
r := CheckPlacements(map[string]Placement{"ace": a, "novox": b}, held, enrolled)
if (len(r) != 0) != refused {
t.Errorf("%s on two nodes: refusals %v, want refused %v", seat, r, refused)
}
}
clash := g14
clash.Modules = append([]PlacedModule{{Module: "ghost", Ports: map[string]int{"80": 18080}}}, g14.Modules...)
clash.Modules[2] = PlacedModule{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "ace", Module: "postgres"}}}
clash.Pins = map[string]Provider{"shell": {Node: "g14", Module: "bash"}}
twice := novox
twice.Node = "stranger"
twice.Decided.Address = "10.77.0.2"
r := CheckPlacements(map[string]Placement{"g14": clash, "novox": novox, "stranger": twice}, held, enrolled)
for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080",
"bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record",
"mesh-store, held once for the mesh, is held on novox and stranger",
"g14 and stranger both have the address 10.77.0.2", "novox, stranger are each placed as it"} {
found := false
for _, line := range r {
found = found || strings.Contains(line, want)
}
if !found {
t.Errorf("no refusal says %q; got %v", want, r)
}
}
})
// ADR 0567 §3: a proposal's candidates, one key `<proposal>.<node>` per node it must hear from, and each node's answer
// in its have.
t.Run("candidates", func(t *testing.T) {
const id = "p-20261028t183000-1a2b3c"
key, err := PlacementCandidateKey(id, "g14")
if err != nil || key != id+".g14" {
t.Fatalf("PlacementCandidateKey = %q, %v", key, err)
}
if p, n, err := ParsePlacementCandidateKey(key); err != nil || p != id || n != "g14" {
t.Fatalf("ParsePlacementCandidateKey = %q, %q, %v", p, n, err)
}
for _, bad := range []string{"g14", ".g14", "p-x.g.14", "p-x.", "P-X.g14", "feat/1.g14"} {
if _, _, err := ParsePlacementCandidateKey(bad); err == nil {
t.Errorf("ParsePlacementCandidateKey(%q) took it", bad)
}
}
for name, c := range map[string]PlacementCandidate{
"placed": {Proposal: id, Placement: &g14},
"removed": {Proposal: id},
"reached": {Proposal: id, Reached: true, Placement: &g14},
} {
raw, err := EncodePlacementCandidate(c)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
back, err := DecodePlacementCandidate(raw)
if err != nil || back.Proposal != id || back.Reached != c.Reached || (back.Placement == nil) != (c.Placement == nil) ||
(c.Placement != nil && !SamePlacement(*back.Placement, g14)) {
t.Errorf("%s: round trip %+v, %v", name, back, err)
}
}
for name, c := range map[string]PlacementCandidate{
"no proposal": {},
"a group": {Proposal: "feat/x"},
"bad place": {Proposal: id, Placement: &Placement{Node: "a.b"}},
} {
if _, err := EncodePlacementCandidate(c); err == nil {
t.Errorf("%s: taken", name)
}
}
answers := map[string]CandidateAnswer{
"same": {Proposal: id, Revision: 3, Verdict: CandidateSame, Same: []string{"app", "node"}},
"differs": {Proposal: id, Revision: 3, Verdict: CandidateDiffers, Differs: []ModuleDiffers{{Module: "app", Resource: "file:/etc/app", Why: "the resource differs"}}},
"refused": {Proposal: id, Revision: 3, Verdict: CandidateRefused, Refused: "unbound postgres"},
"added": {Proposal: id, Revision: 1, Verdict: CandidateDiffers, Same: []string{"node"}, Added: []ModuleAdded{{Module: "app", Unissued: []string{"app/token"}}}, Removed: []string{"old"}},
"a value": {Proposal: id, Revision: 1, Verdict: CandidateRefused, Same: []string{"node"}, Refusals: []ModuleRefused{{Module: "db", Why: "stands in for db/password"}}},
// A head that deletes the node's placement file: the node returns to the controller's compositions (issue 1126,
// the seed's revert, mesh-catalog #526).
"returns": {Proposal: id, Revision: 1, Verdict: CandidateSame, Returns: ReturnsToController, Same: []string{"app", "node"}},
"returns, the controller's differs": {Proposal: id, Revision: 1, Verdict: CandidateDiffers, Returns: ReturnsToController, Same: []string{"node"},
Differs: []ModuleDiffers{{Module: "app", Resource: "file:/etc/app", Why: "the resource differs"}}, Removed: []string{"old"}},
}
n := HaveNode{Composed: "sha256:" + strings.Repeat("a", 64), Sealed: []string{"box"}, Candidates: answers,
Composes: ComposesOwn, ComposesWhy: "the catalogue places it (placement.n1)"}
raw, err := EncodeHaveNode(n)
if err != nil {
t.Fatal(err)
}
back, err := DecodeHaveNode(raw)
if err != nil || !SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Candidates: answers,
Composes: ComposesOwn, ComposesWhy: n.ComposesWhy}) {
t.Fatalf("a node's answers changed on the way: %+v, %v", back, err)
}
if SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Composes: ComposesOwn, ComposesWhy: n.ComposesWhy}) {
t.Error("a node's have without its answers counts as the same")
}
if SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Candidates: answers,
Composes: ComposesController, ComposesWhy: n.ComposesWhy}) {
t.Error("a node's have composing otherwise counts as the same")
}
if _, err := EncodeHaveNode(HaveNode{Composes: "both"}); err == nil {
t.Error("a node composing neither its own nor the controller's was taken")
}
// CompareComposed: the bus's user list is left out on both sides; anything else that differs is named.
file := func(id, content string) json.RawMessage {
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"0600","content":"` + content + `"}`)
}
fp := "src2:" + strings.Repeat("a", 64)
comp := func(rs ...json.RawMessage) Composed {
return Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs}
}
for name, tc := range map[string]struct {
composed, running Composed
resource, why string
}{
"user list alone": {comp(file("conf", "a"), file("nats.bus-users", "stand-in")), comp(file("conf", "a"), file("nats.bus-users", "real")), "", ""},
"a resource": {comp(file("conf", "a")), comp(file("conf", "b")), "conf", "the resource differs"},
"one side": {comp(file("conf", "a"), file("more", "x")), comp(file("conf", "a")), "more", "composed, and not in what runs"},
"order": {comp(file("conf", "a"), file("more", "x")), comp(file("more", "x"), file("conf", "a")), "", "the same resources in another order"},
} {
c := CompareComposed(map[string]Composed{"novox.nats." + fp: tc.composed}, []Composed{tc.running})
if tc.why == "" {
if len(c.Differs) != 0 || len(c.Same) != 1 {
t.Errorf("%s: %+v", name, c)
}
} else if len(c.Differs) != 1 || c.Differs[0].Resource != tc.resource || c.Differs[0].Why != tc.why ||
c.Differs[0].Composed == "" || c.Differs[0].Running == "" || c.Differs[0].Composed == c.Differs[0].Running {
t.Errorf("%s: differs %+v, want %s: %s with both digests", name, c.Differs, tc.resource, tc.why)
}
}
// Issue 1126, the shadow shows its work: every differing resource with both bodies, each value a secret could be
// in masked (never a secret in an answer), saying whether the two sides hold the same value.
sealedFile := func(id, sealed, mode string) json.RawMessage {
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"` + mode + `","sealed":"` + sealed + `"}`)
}
// Issue 1126, C1: a sealed value is judged by the value it opens to at its own slot: b sealed again (box-2 and
// box-3 open to one value) is no difference; d holds at its slot a box of another value, though that value is
// the one b holds, and differs at its path `sealed`.
sameBox := func(a, b string) (same, known bool) {
return (a == "box-2" && b == "box-3") || a == b, true
}
shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"),
file("c", "x"), sealedFile("d", "box-2", "0600"))},
[]Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))},
sameBox)
if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 {
t.Fatalf("every differing resource is not shown, or b sealed again is shown: %+v", shown.Differs)
}
if c := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("b", "box-2", "0600"))},
[]Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 {
t.Errorf("a value sealed again at its own slot is not the same: %+v", c)
}
for _, row := range []struct {
id, has, hasNot string
paths []string
}{
{"a", `"mode":"0600"`, "box-1", []string{"mode"}},
{"c", `"content":"x"`, "", []string{"content"}},
{"d", "<masked: another value>", "box-2", []string{"sealed"}},
} {
var body ResourceBodies
for _, b := range shown.Differs[0].Bodies {
if b.ID == row.id {
body = b
}
}
both := body.Composed + body.Running
if !strings.Contains(both, row.has) || (row.hasNot != "" && strings.Contains(both, row.hasNot)) ||
!slices.Equal(body.Paths, row.paths) {
t.Errorf("%s shown as %+v, want paths %v", row.id, body, row.paths)
}
}
// Issue 1126, C1, the seed-7 probe: a file naming secrets carries each sealed value in its `secrets`, and each entry
// is a slot of its own. The shape is intoFile's output, captured from catalogue for a file naming two secrets
// (2026-10-28), with the module a composition names each resource of; only the boxes are replaced.
withSecrets := func(db, key string) json.RawMessage {
return json.RawMessage(`{"content":"[database]\npassword = ${secret:postgres-database}\n[security]\nsecret_key = ${secret:secret-key}\n",` +
`"id":"grafana.ini","mode":"0600","module":"nats","path":"/etc/grafana/grafana.ini","secrets":{"postgres-database":"` + db +
`","secret-key":"` + key + `"},"type":"file"}`)
}
if c := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-1"))},
[]Composed{comp(withSecrets("box-3", "box-1"))}, sameBox); len(c.Same) != 1 {
t.Errorf("a secret sealed again at its own entry is not the same: %+v", c)
}
if kept := KeepSeals(comp(withSecrets("box-2", "box-1")), SealsIn(comp(withSecrets("box-3", "box-1"))), sameBox); !bytes.Equal(kept.Resources[0], withSecrets("box-3", "box-1")) {
t.Errorf("the box that runs is not kept byte for byte: %s", kept.Resources[0])
}
if s := SealsIn(comp(withSecrets("box-2", "box-1"))); s[SealSlot("grafana.ini", SecretSlotField("postgres-database"))] != "box-2" || len(s) != 2 {
t.Errorf("the secrets' slots are %v", s)
}
// Another value at one entry differs there alone, each entry masked by what it opens to.
// box-2 and box-4 open to one value, so the box that runs is kept there; box-5 and box-6 are two values.
mixed := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-5"))},
[]Composed{comp(withSecrets("box-4", "box-6"))}, func(a, b string) (bool, bool) {
return (a == "box-2" && b == "box-4") || a == b, true
})
if len(mixed.Differs) != 1 || len(mixed.Differs[0].Bodies) != 1 {
t.Fatalf("a secret of another value is not shown: %+v", mixed)
}
if body := mixed.Differs[0].Bodies[0]; !slices.Equal(body.Paths, []string{"secrets.secret-key"}) ||
!strings.Contains(body.Composed, `"secret-key":"<masked: another value>"`) || strings.Contains(body.Composed+body.Running, "box-") ||
!strings.Contains(body.Running, `"postgres-database":"<masked: the same on both sides>"`) {
t.Errorf("a file's secrets shown as %+v", body)
}
if c, _ := MaskedPair(withSecrets("box-2", "box-5"), withSecrets("box-4", "box-6"), func(a, b string) (bool, bool) {
return a == "box-2", true
}); !strings.Contains(c, `"postgres-database":"<masked: another seal of the same value>"`) || !strings.Contains(c, `"secret-key":"<masked: another value>"`) {
t.Errorf("each secret is not masked by what it opens to: %s", c)
}
// Only a file's secrets are split by entry: a vault box (an object under `vault-sealed`) is one masked word and one
// path, judged whole by what it opens to (rev-seed-same-3, round 1).
box := func(ct string) json.RawMessage {
return json.RawMessage(`{"id":"core-box","type":"file","module":"nats","path":"/etc/core","mode":"0600","vault-sealed":{"v":1,"eph":"e-` +
ct + `","ct":"` + ct + `"}}`)
}
vault := CompareComposedWith(map[string]Composed{"k": comp(box("c1"))}, []Composed{comp(box("c2"))},
func(a, b string) (bool, bool) {
return false, strings.Contains(a, `"ct":"c1"`) && strings.Contains(b, `"ct":"c2"`)
})
if len(vault.Differs) != 1 || len(vault.Differs[0].Bodies) != 1 {
t.Fatalf("a box of another value is not shown: %+v", vault)
}
if body := vault.Differs[0].Bodies[0]; !slices.Equal(body.Paths, []string{"vault-sealed"}) ||
!strings.Contains(body.Composed, `"vault-sealed":"<masked: another value>"`) || strings.Contains(body.Composed+body.Running, "c1") {
t.Errorf("a vault box shown as %+v", body)
}
// Issue 1126, T1: the paths are found on the whole bodies, before they are cut; JSON in a string is walked into, a
// key a secret could be under is named and never shown.
long := strings.Repeat("x", MaxBody)
conf := func(link, password string) json.RawMessage {
inner, _ := json.Marshal(map[string]any{"pad": long, "links": []string{"a", link}, "password": password})
outer, _ := json.Marshal(map[string]any{"id": "conf", "type": "file", "module": "nats", "path": "/etc/conf",
"mode": "0600", "content": string(inner)})
return outer
}
far := CompareComposedWith(map[string]Composed{"k": comp(conf("b", "hunter2"))}, []Composed{comp(conf("c", "hunter3"))}, nil)
if len(far.Differs) != 1 || len(far.Differs[0].Bodies) != 1 ||
!slices.Equal(far.Differs[0].Bodies[0].Paths, []string{"content.links.1", "content.password"}) ||
!strings.Contains(far.Differs[0].Bodies[0].Composed, "cut at") || strings.Contains(fmt.Sprint(far), "hunter") {
t.Errorf("a difference past the cut: %+v", far.Differs)
}
// And an answer too large for the node's key is fitted: the texts go first, the verdict and the paths stay.
big := CandidateAnswer{Proposal: "p-x1", Revision: 1, Verdict: CandidateDiffers, Differs: far.Differs}
if fitted := FitAnswer(big, 4<<10); len(fitted.Differs[0].Bodies[0].Composed) != 0 || len(fitted.Differs[0].Bodies[0].Paths) != 2 ||
fitted.Verdict != CandidateDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" {
t.Errorf("fitted as %+v", fitted)
}
if c, r := MaskedPair(json.RawMessage(`{"env":{"DB_PASSWORD":"hunter2","PORT":"80"},"list":[{"token":"t1"}]}`),
json.RawMessage(`{"env":{"DB_PASSWORD":"hunter3","PORT":"81"},"list":[{"token":"t1"}]}`), nil); strings.Contains(c+r, "hunter") ||
strings.Contains(c+r, `"t1"`) || !strings.Contains(r, `"PORT":"81"`) || !strings.Contains(c, "<masked: differs>") ||
!strings.Contains(c, "<masked: the same on both sides>") {
t.Errorf("masked as %s and %s", c, r)
}
// Issue 1126, cause C4: the own secrets whose two sides open to two values are read from the shown bodies, and
// every body so masked is counted, own or not.
// The ids are as a node runs them, its module's name first (captured from novox's shadow answer, 2026-10-28:
// `only-office.needs-jwt`); another module's need, named here, is not nats's.
drift := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("nats.needs-jwt", "box-1", "0600"),
sealedFile("nats.needs-salt", "box-4", "0600"), sealedFile("nats.conf", "box-6", "0600"),
sealedFile("other.needs-key", "box-8", "0600"))},
[]Composed{comp(sealedFile("nats.needs-jwt", "box-2", "0600"), sealedFile("nats.needs-salt", "box-5", "0600"),
sealedFile("nats.conf", "box-7", "0600"), sealedFile("other.needs-key", "box-9", "0600"))},
func(a, b string) (same, known bool) { return a == "box-4", true })
got := CandidateAnswer{Proposal: "p-x1", Revision: 1, Differs: drift.Differs}
if own := OwnValuesDiffer(got); len(own) != 1 || own[0] != (OwnValueDiffers{Module: "nats", Name: "jwt"}) {
t.Errorf("the own secrets with another value are %+v, want nats jwt alone", own)
}
if n := ValuesDiffer(got); n != 3 {
t.Errorf("%d bodies hold another value, want 3 (jwt, conf and other's key)", n)
}
for id, want := range map[string]string{"nats.needs-jwt": "jwt", "needs-jwt": "jwt", "other.needs-jwt": "",
"nats.conf": "", "nats.needs-": ""} {
if name, own := OwnNeedName("nats", id); name != want && (own || want != "") {
t.Errorf("%s names nats's own secret %q (%v), want %q", id, name, own, want)
}
}
if w := WithValuesDiffer(map[string]CandidateAnswer{"g": got}); w["g"].ValuesDiffer != 3 {
t.Errorf("the answer carries values_differ %d", w["g"].ValuesDiffer)
}
if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" {
t.Errorf("the node's own composition run and not composed is removed: %+v", c)
}
if c := CompareComposed(map[string]Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" {
t.Errorf("a composition not run is added: %+v", c)
}
for name, a := range map[string]CandidateAnswer{
"no head": {Verdict: CandidateSame},
"same with a diff": {Proposal: "p-x1", Revision: 1, Verdict: CandidateSame, Differs: answers["differs"].Differs},
"differs with none": {Proposal: "p-x1", Revision: 1, Verdict: CandidateDiffers},
"refused no reason": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused},
"unknown verdict": {Proposal: "p-x1", Revision: 1, Verdict: "maybe"},
"diff with no module": {Proposal: "p-x1", Revision: 1, Verdict: CandidateDiffers, Differs: []ModuleDiffers{{Why: "x"}}},
"same with an added": {Proposal: "p-x1", Revision: 1, Verdict: CandidateSame, Added: []ModuleAdded{{Module: "a"}}},
"differs refusing": {Proposal: "p-x1", Revision: 1, Verdict: CandidateDiffers, Removed: []string{"a"}, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}},
"refused and modules": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Refused: "x", Same: []string{"a"}},
"returns refusing": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Returns: ReturnsToController, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}},
"returns refused": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Returns: ReturnsToController, Refused: "x"},
} {
if _, err := EncodeHaveNode(HaveNode{Candidates: map[string]CandidateAnswer{"g": a}}); err == nil {
t.Errorf("%s: taken", name)
}
}
})
// ADR 0567 §2, §8: a proposal's changes laid over the current keys, what waits for the lease write, and the proposal's
// encoding and refusals.
t.Run("proposal", func(t *testing.T) {
current := map[string]Placement{"g14": g14, "novox": novox}
laid, findings := LayChanges(current, []PlacementProposalChange{
{Op: OpPlace, Node: "g14", Module: "new"},
{Op: OpTakeOff, Node: "g14", Module: "zsh"},
{Op: OpUnpin, Node: "g14", Provision: "shell"},
{Op: OpDecide, Node: "novox", Field: "site", Value: json.RawMessage(`"attic"`)},
})
if len(findings) != 0 || len(laid) != 2 || !reflect.DeepEqual(laid["g14"].Assigned(), []string{"app", "new"}) ||
len(laid["g14"].Holds) != 0 || laid["g14"].Pins != nil || laid["novox"].Decided.Site != "attic" || !laid["novox"].Decided.Hub {
t.Fatalf("laid %+v, %v", laid, findings)
}
if !reflect.DeepEqual(current["g14"].Assigned(), []string{"zsh", "app"}) {
t.Error("laying changed the current placement")
}
if fresh, f := LayChanges(current, []PlacementProposalChange{{Op: OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
t.Errorf("a node with no key: %+v, %v", fresh, f)
}
for name, c := range map[string]struct {
change PlacementProposalChange
want string
}{
"not placed": {PlacementProposalChange{Op: OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
"no such field": {PlacementProposalChange{Op: OpDecide, Node: "g14", Field: "colour", Value: json.RawMessage(`"x"`)}, "no decided field"},
"wrong type": {PlacementProposalChange{Op: OpDecide, Node: "g14", Field: "hub", Value: json.RawMessage(`"x"`)}, "does not take"},
"unknown op": {PlacementProposalChange{Op: "move", Node: "g14"}, "no change of placement"},
"pin of nobody": {PlacementProposalChange{Op: OpPin, Node: "g14", Provision: "x"}, "provider"},
"not held": {PlacementProposalChange{Op: OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
"bad port": {PlacementProposalChange{Op: OpPlace, Node: "g14", Module: "x", Ports: map[string]int{"a": 1}}, "port"},
"seed of another": {PlacementProposalChange{Op: OpSeed, Node: "g14", Whole: &novox}, "seed"},
} {
if _, f := LayChanges(current, []PlacementProposalChange{c.change}); len(f) != 1 || !strings.Contains(f[0], c.want) {
t.Errorf("%s: %v, want a finding naming %q", name, f, c.want)
}
}
for change, waits := range map[*PlacementProposalChange]bool{
{Op: OpPlace, Node: "g14", Module: "x"}: false, {Op: OpTakeOff, Node: "g14", Module: "x"}: false,
{Op: OpPin, Node: "g14"}: false, {Op: OpUnpin, Node: "g14"}: false, {Op: OpSeed, Node: "g14"}: false,
{Op: OpPlace, Node: "g14", Module: "x", Ports: map[string]int{"80": 80}}: true,
{Op: OpPlace, Node: "g14", Module: "x", Bindings: map[string]Provider{"p": {}}}: true,
{Op: OpHold, Node: "g14"}: true, {Op: OpDecide, Node: "g14"}: true,
{Op: OpConfig, Node: "g14", Values: map[string]any{"puid": 1}}: false, {Op: OpConfig, Node: "g14", MeshValues: map[string]any{"a": 1}}: false,
{Op: OpConfig, Node: "g14", Layer: map[string]any{"places": 1}}: true, {Op: OpConfig, Node: "g14", MeshWide: map[string]any{"accesses": 1}}: true,
} {
if got := change.BeforeLease(); (got != "") != waits || (waits && !strings.Contains(got, "lease write")) {
t.Errorf("%+v before the lease write: %q", *change, got)
}
}
const id = "p-20261028t183000-1a2b3c"
if got := NewProposalID(time.Date(2026, 10, 28, 18, 30, 0, 0, time.UTC)); !strings.HasPrefix(got, "p-20261028t183000-") || CheckProposalID(got) != nil {
t.Errorf("NewProposalID = %q", got)
}
p := PlacementProposal{ID: id, By: "operator@anchor", Why: "move the app", At: time.Unix(1, 0).UTC(),
Changes: []PlacementProposalChange{{Op: OpTakeOff, Node: "g14", Module: "zsh"}}, Base: map[string]uint64{"g14": 4},
State: ProposalAnswering}
raw, err := EncodePlacementProposal(p)
if err != nil {
t.Fatal(err)
}
if back, err := DecodePlacementProposal(raw); err != nil || back.ID != id || back.Base["g14"] != 4 || !reflect.DeepEqual(back.Nodes(), []string{"g14"}) {
t.Fatalf("round trip %+v, %v", back, err)
}
seed := p
seed.Seed, seed.ExportDigest = true, ExportDigest(current)
seed.Changes = []PlacementProposalChange{{Op: OpSeed, Node: "g14", Whole: &g14}}
if _, err := EncodePlacementProposal(seed); err != nil {
t.Errorf("the seed: %v", err)
}
if ExportDigest(current) != ExportDigest(map[string]Placement{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
t.Error("the export digest depends on order or not on the placements")
}
back := p
back.HandBack, back.Changes = true, nil
if _, err := EncodePlacementProposal(back); err != nil {
t.Errorf("a hand-back: %v", err)
}
if k, err := PulseAnswerKey(id, "g14"); err != nil || k != "answer."+id+".g14" {
t.Errorf("PulseAnswerKey = %q, %v", k, err)
}
if k, err := PulseAskKey(id); err != nil || k != "ask."+id || PlacementPulsesBucket != "mesh-delivery-assigner_pulses" {
t.Errorf("PulseAskKey = %q, %v", k, err)
}
if ChangesDigest(p) == ChangesDigest(seed) {
t.Error("two proposals' changes digest alike")
}
for name, mut := range map[string]func(*PlacementProposal){
"no id": func(p *PlacementProposal) { p.ID = "" },
"no proposer": func(p *PlacementProposal) { p.By = "" },
"no why": func(p *PlacementProposal) { p.Why = " " },
"no change": func(p *PlacementProposal) { p.Changes = nil },
"no base": func(p *PlacementProposal) { p.Base = map[string]uint64{"novox": 1} },
"no state": func(p *PlacementProposal) { p.State = "" },
"asked, no ask": func(p *PlacementProposal) { p.State = ProposalAsked },
"seed mixed": func(p *PlacementProposal) { p.Seed, p.ExportDigest = true, "d" },
"bad change": func(p *PlacementProposal) { p.Changes[0].Op = "swap" },
"seed, no digest": func(p *PlacementProposal) { *p = seed; p.ExportDigest = "" },
"hand-back with changes": func(p *PlacementProposal) { p.HandBack = true },
} {
q := p
q.Changes = append([]PlacementProposalChange(nil), p.Changes...)
mut(&q)
if _, err := EncodePlacementProposal(q); err == nil {
t.Errorf("%s: taken", name)
}
}
})
}