Files
mesh-catalog/modules/mesh-provisioner/cmd/mesh-provisioner/composed_test.go
T

216 lines
8.7 KiB
Go

package main
// The provisioner makes every plan from the provisions the node-engine's published compositions carry (novox/hq ADR
// 0593 §6, §8), with the plans and provisions it wrote when it composed them itself.
//
// The fixtures were captured on 2026-11-02 from the holder as it stood before (frombus.PlansHeldTwice over mesh-sdk's
// frombustest.Provisioned mesh: restic on n1 requiring postgres-database from postgres there, the vault on n2):
// testdata/<case>today-composed.json is what that composition answered at the node's wants and at its newest builds,
// testdata/<case>today-states.golden.json what the holder then wrote. "ahead-" has restic wanted at an older build
// that requires nothing, so its key is ahead of its want and carries its plan entry (issue 1126 review 1).
import (
"context"
"encoding/json"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"os"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// today is a captured composition: the node, the builds its wants named, and the plans at them and at the newest.
type today struct {
Node string `json:"node"`
KeptAt map[string]string `json:"kept-at"`
AtWants struct {
Plans map[string]catalogue.Plan `json:"Plans"`
Held map[string]string `json:"Held"`
} `json:"at-wants"`
Newest struct {
Plans map[string]catalogue.Plan `json:"Plans"`
} `json:"newest"`
}
// fpsOf is frombustest's modules and the fingerprints they are built at (frombustest.FPs).
var fpsOf = map[string]string{"restic": "src2:" + strings.Repeat("3", 64), "mesh-provisioner-client": "src2:" + strings.Repeat("4", 64),
"store": "src2:" + strings.Repeat("5", 64), "postgres": "src2:" + strings.Repeat("8", 64)}
// published puts on the store what n1's engine publishes for the captured case: each placed module at its newest done
// build, and each wanted one at its want, each key carrying its plan.
func published(t *testing.T, s *memStore, c today) {
t.Helper()
put := func(module, fp string, plan catalogue.Plan, has bool) {
v := map[string]any{"v": 1, "node": c.Node, "module": module, "fp": fp, "resources": []any{}}
if has {
v["plan"] = plan
}
raw, _ := json.Marshal(v)
s.write(t, ComposedRead, mustKey(composedKey(c.Node, module, fp)), string(raw))
}
for module, fp := range fpsOf {
plan, has := c.Newest.Plans[module]
put(module, fp, plan, has)
}
for module, fp := range c.KeptAt {
plan, has := c.AtWants.Plans[module]
put(module, fp, plan, has)
}
s.write(t, ComposedRead, mustKey(engineclient.HaveNodeKey(c.Node)), `{"v":1,"node":"`+c.Node+`","resources":[]}`)
}
// goldenMesh is the store as the provisioner reads it for frombustest's mesh: the indexer's modules, n1's placement, the
// vault's on n2, restic's want.
func goldenMesh(t *testing.T, c today) *memStore {
t.Helper()
s := newStore()
for module, fp := range fpsOf {
// A provider's manifest says whether it keeps entries: postgres's, as frombustest has it, keeps none.
raw := `{"module":"` + module + `","version":"1"}`
if module == "postgres" {
raw = `{"module":"postgres","version":"1","provides":[{"name":"postgres-database","scope":"mesh"}],` +
`"serves":{"postgres-database":{"port":5432}}}`
}
digest := catalogclient.ManifestDigest([]byte(raw))
value, _ := catalogclient.EncodeManifest(catalog.ManifestsByDigestValue{Module: module, Manifest: raw})
s.write(t, ManifestsRead, mustKey(catalogclient.ManifestsKey(digest)), string(value))
s.write(t, ModulesRead, module, `{"v":1,"fp2":"`+fp+`","manifest":"`+digest+`"}`)
}
s.write(t, PlacementRead, "placement."+c.Node, `{"v":1,"node":"`+c.Node+`","decided":{},"modules":[{"module":"store"},`+
`{"module":"postgres"},{"module":"mesh-provisioner-client"},{"module":"restic"}]}`)
s.write(t, PlacementRead, "placement.n2", `{"v":1,"node":"n2","decided":{},"modules":[{"module":"mesh-vault"}]}`)
s.write(t, WantsRead, "want."+c.Node+".restic", `{"v":1,"fp":"`+c.KeptAt["restic"]+`","self":true}`)
return s
}
// The plans and provisions made from the published compositions are today's, value for value, with a key ahead of its
// want carrying its plan entry; a pass over the same compositions writes nothing.
func TestThePlansFromCompositionsAreTodays(t *testing.T) {
for _, name := range []string{"", "ahead-"} {
t.Run("case "+name, func(t *testing.T) {
var c today
readJSON(t, "testdata/"+name+"today-composed.json", &c)
var golden map[string]map[string]json.RawMessage
readJSON(t, "testdata/"+name+"today-states.golden.json", &golden)
s := goldenMesh(t, c)
published(t, s, c)
now := time.Date(2026, 11, 2, 18, 0, 0, 0, time.UTC)
h := &Holder{Store: s, Asks: &router{}, Logf: t.Logf, Now: func() time.Time { return now }}
h.Compose = FromCompositions(s)
out, err := h.ComposeNode(context.Background(), c.Node)
if err != nil {
t.Fatal(err)
}
if len(out.Problems)+len(out.Held) > 0 {
t.Errorf("problems %v, held %v", out.Problems, out.Held)
}
for state, want := range golden {
keys, _ := s.Keys(state)
if len(keys) != len(want) {
t.Errorf("%s holds %v, today %d keys", state, keys, len(want))
}
for key, raw := range want {
e, _ := s.Get(state, key)
if e == nil || !sameBytes(t, e.Value, raw) {
t.Errorf("%s %s:\n got %s\nwant %s", state, key, valueOf(e), raw)
}
}
}
again, err := h.ComposeNode(context.Background(), c.Node)
if err != nil || len(again.Plans)+len(again.Gone)+len(again.Changed) > 0 {
t.Errorf("a second pass over the same compositions wrote %+v (%v)", again, err)
}
})
}
}
// A composition the engine refused at the wanted build holds its module, its last plan kept and its words in `refused`;
// one the engine has not published yet holds it too, saying so; a module whose newest build is refused keeps its
// plan at its want.
func TestARefusedOrMissingCompositionHoldsItsModule(t *testing.T) {
var c today
readJSON(t, "testdata/today-composed.json", &c)
s := goldenMesh(t, c)
published(t, s, c)
h := &Holder{Store: s, Asks: &router{}, Logf: t.Logf, Now: time.Now}
h.Compose = FromCompositions(s)
if _, err := h.ComposeNode(context.Background(), c.Node); err != nil {
t.Fatal(err)
}
plan, _ := s.Get(PlansState, "n1.restic")
key := mustKey(composedKey("n1", "restic", fpsOf["restic"]))
s.write(t, ComposedRead, key, `{"v":1,"node":"n1","module":"restic","fp":"`+fpsOf["restic"]+`","resources":[],`+
`"refused":"restic requires postgres-database and no module offers it"}`)
out, err := h.ComposeNode(context.Background(), c.Node)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out.Held["restic"], "no module offers it") {
t.Errorf("the refused module is held as %v", out.Held)
}
if e, _ := s.Get(PlansState, "n1.restic"); e == nil || string(e.Value) != string(plan.Value) {
t.Errorf("the held module's plan was not kept: %s", valueOf(e))
}
if e, _ := s.Get(RefusedState, "n1.restic"); e == nil || !strings.Contains(string(e.Value), "no module offers it") {
t.Errorf("refused says %s", valueOf(e))
}
if p, _ := s.Get(ProvisionsState, "n1.restic.postgres-database"); p == nil || strings.Contains(string(p.Value), "retiring") {
t.Errorf("the held module's key: %s", valueOf(p))
}
// Not published yet: the want moved to a build the engine has not composed.
other := "src2:" + strings.Repeat("9", 64)
s.write(t, WantsRead, "want.n1.restic", `{"v":1,"fp":"`+other+`","self":true}`)
out, err = h.ComposeNode(context.Background(), c.Node)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out.Held["restic"], "not published") {
t.Errorf("the unpublished composition holds restic as %v", out.Held)
}
if e, _ := s.Get(PlansState, "n1.restic"); e == nil || string(e.Value) != string(plan.Value) {
t.Errorf("the plan was not kept while unpublished: %s", valueOf(e))
}
}
func readJSON(t *testing.T, path string, v any) {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(raw, v); err != nil {
t.Fatal(err)
}
}
func sameBytes(t *testing.T, a, b []byte) bool {
t.Helper()
var x, y any
if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil {
return false
}
ra, _ := json.Marshal(x)
rb, _ := json.Marshal(y)
return string(ra) == string(rb)
}
func valueOf(e *Entry) string {
if e == nil {
return "(none)"
}
return string(e.Value)
}
// composedKey is a module's key in the node-engine's composed state, from the fingerprint the test names it by.
func composedKey(node, module, fp string) (string, error) {
part, err := catalogclient.KeyPart(fp)
if err != nil {
return "", err
}
return engineclient.ComposedKey(node, module, part)
}