The engine's Composed keeps the catalogue's Plan and the declaration types and goes through nodeengineclient's codec; the fingerprint key part is meshcatalogclient's. The candidate answer is nodeengine.CandidateAnswer; the have still holds it through an interim JSON bridge (link.interimAnswers) until the Have group moves. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
169 lines
10 KiB
Go
169 lines
10 KiB
Go
package compositions
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// What the node-engine finds when it compares the compositions it made with the ones it runs (issue 1126): the bus's user
|
|
// list left out, a resource that differs named with both digests, a value sealed again judged by what it opens to, every
|
|
// differing body shown with its secrets masked, and the paths found on the whole bodies. Moved from mesh-sdk's delivery
|
|
// package with the code (ADR 0634 §1: only the node-engine compares compositions).
|
|
func TestComparingCompositions(t *testing.T) {
|
|
// 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 seat.CandidateBodies
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
}
|