mesh-provisioner: make every plan from the node-engine's published compositions

The node-engine alone composes (ADR 0593 §6, §8), so the provisioner stops
composing with frombus and fromruntime and reads each module's plan from
composed-<node> (read as mesh-host.composed): at its want, or the build its
have runs, and at the indexer's newest build, so a key ahead of its want
still carries its plan entry. A refused or not yet published composition
holds the module with its last plan kept. The golden test proves the plans
and provisions are today's, captured from the composing holder before the
change. It no longer reads the builds or the resolved values.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 18:20:39 +01:00
parent c52afdf88b
commit 2d2eb5dffb
13 changed files with 790 additions and 394 deletions
@@ -1,225 +0,0 @@
package main
// BenchmarkComposeNode times ComposeNode of an unchanged node over a mesh captured from the bus (issue 1954): the
// placement, wants, haves, the indexer's modules and manifests, the builder's builds, mesh-config's resolved values and
// the templates archives, read with the read tools (mesh-placement.placement, mesh-delivery-view.assigner_wants,
// node_haves, indexer_modules, builder_build, mesh-config.config) and the manifests and archives by digest. The capture
// is not kept in this repository (a few megabytes, and the mesh's own addresses and keys): the benchmark reads the
// directory MESH_COMPOSE_CAPTURE names and is skipped without it. MESH_COMPOSE_NODE names the node (novox when unset).
//
// The capture's layout: placement.json, wants.json, haves.json, modules.json and config.json as those tools answer,
// builds/<module>.<hex>.json as builder_build answers, manifests/<digest> and archives/<hex> as their bytes.
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/compose/fromruntime"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// countingStore counts the holder's reads of its own states (and the providers' entries), each a bus request live.
type countingStore struct {
Store
gets, keys atomic.Int64
}
func (c *countingStore) Get(state, key string) (*Entry, error) {
c.gets.Add(1)
return c.Store.Get(state, key)
}
func (c *countingStore) Keys(state string) ([]string, error) {
c.keys.Add(1)
return c.Store.Keys(state)
}
// captured is the captured mesh as the states the holder reads, and the archives by digest.
func captured(b *testing.B, dir string) (map[string]map[string][]byte, map[string][]byte) {
b.Helper()
read := func(name string, into any) {
raw, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
b.Fatal(err)
}
if err := json.Unmarshal(raw, into); err != nil {
b.Fatalf("%s: %v", name, err)
}
}
states := map[string]map[string][]byte{}
put := func(state, key string, raw []byte) {
if states[state] == nil {
states[state] = map[string][]byte{}
}
states[state][key] = raw
}
var placement struct {
Keys map[string]struct {
Placement json.RawMessage `json:"placement"`
} `json:"keys"`
}
read("placement.json", &placement)
for node, k := range placement.Keys {
var p delivery.Placement
if err := json.Unmarshal(k.Placement, &p); err != nil {
b.Fatal(err)
}
p.Node = node
raw, err := delivery.EncodePlacement(p)
if err != nil {
b.Fatal(err)
}
put(PlacementRead, "placement."+node, raw)
}
var wants struct {
Wants map[string]json.RawMessage `json:"wants"`
}
read("wants.json", &wants)
for k, v := range wants.Wants {
put(WantsRead, k, v)
}
var haves struct {
Keys map[string]json.RawMessage `json:"keys"`
}
read("haves.json", &haves)
for k, v := range haves.Keys {
put(HaveRead, k, v)
}
var modules struct {
Modules map[string]json.RawMessage `json:"modules"`
}
read("modules.json", &modules)
for m, v := range modules.Modules {
key, err := delivery.ModulesKey(m)
if err != nil {
b.Fatal(err)
}
put(ModulesRead, key, v)
}
var config struct {
Resolved []struct {
Key, Module, Node, Source string
Value any
} `json:"resolved"`
}
read("config.json", &config)
for _, r := range config.Resolved {
raw, _ := json.Marshal(map[string]any{"value": r.Value, "source": r.Source})
put(ResolvedRead, r.Node+"."+r.Module+"."+r.Key, raw)
}
manifests, _ := filepath.Glob(filepath.Join(dir, "manifests", "*"))
for _, f := range manifests {
raw, err := os.ReadFile(f)
if err != nil {
b.Fatal(err)
}
digest := filepath.Base(f)
v, err := delivery.EncodeManifest(delivery.Manifest{Module: "captured", JSON: string(raw)})
if err != nil {
b.Fatal(err)
}
key, err := delivery.ManifestsKey(digest)
if err != nil {
b.Fatal(err)
}
put(ManifestsRead, key, v)
}
builds, _ := filepath.Glob(filepath.Join(dir, "builds", "*.json"))
for _, f := range builds {
var bb struct {
Module string `json:"module"`
Fingerprint string `json:"fingerprint"`
Build json.RawMessage `json:"build"`
}
read(strings.TrimPrefix(f, dir+"/"), &bb)
key, err := delivery.BuilderBuildKey(bb.Module, bb.Fingerprint)
if err != nil {
b.Fatal(err)
}
put(BuildsRead, key, bb.Build)
}
archives := map[string][]byte{}
files, _ := filepath.Glob(filepath.Join(dir, "archives", "*"))
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
b.Fatal(err)
}
archives[filepath.Base(f)] = raw
}
return states, archives
}
func BenchmarkComposeNode(b *testing.B) {
dir := os.Getenv("MESH_COMPOSE_CAPTURE")
if dir == "" {
b.Skip("MESH_COMPOSE_CAPTURE names no captured mesh")
}
node := os.Getenv("MESH_COMPOSE_NODE")
if node == "" {
node = "novox"
}
states, archives := captured(b, dir)
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
for k, v := range states[state] {
if err := handle(stdio.StateChange{State: state, Key: k, Op: stdio.OpPut, Value: v, Current: true}); err != nil {
return err
}
}
return nil
}, func(state, key string) ([]byte, bool, error) {
v, ok := states[state][key]
return v, ok, nil
})
fetch := fromruntime.KeptFetch(func(_ context.Context, url string) ([]byte, error) {
_, hex, _ := strings.Cut(url, "sha256:")
raw, ok := archives[hex]
if !ok {
return nil, os.ErrNotExist
}
return raw, nil
})
own := &countingStore{Store: newStore()}
h := &Holder{Store: Watching(own, ws), Compose: composerOver(frombus.StatesReader(fromruntime.Watched{WatchedStates: ws}, fetch)),
Now: time.Now, Logf: func(string, ...any) {}}
// Composed twice before it is timed: a provision's state follows the states of those it depends on, which the first
// composition writes, so the second may still change a key; from then on nothing changes.
first, err := h.ComposeNode(context.Background(), node)
if err != nil {
b.Fatal(err)
}
if second, err := h.ComposeNode(context.Background(), node); err != nil {
b.Fatal(err)
} else if len(second.Changed) > 0 {
b.Logf("the second composition of %s changed %v", node, second.Changed)
}
b.Logf("first composition of %s: %d plans written, %d held, %d problems, %d keys changed", node, len(first.Plans),
len(first.Held), len(first.Problems), len(first.Changed))
for _, p := range first.Problems {
b.Logf("problem: %s", p)
}
own.gets.Store(0)
own.keys.Store(0)
b.ResetTimer()
var c Composed
for b.Loop() {
c, err = h.ComposeNode(context.Background(), node)
if err != nil {
b.Fatal(err)
}
if len(c.Plans)+len(c.Gone)+len(c.Changed) > 0 {
b.Fatalf("an unchanged node composed changes: %+v", c)
}
}
b.Logf("the last composition of %s: %s", node, TookSaid(c.Took))
b.ReportMetric(float64(own.gets.Load())/float64(b.N), "own-gets/op")
b.ReportMetric(float64(own.keys.Load())/float64(b.N), "own-keys/op")
}
@@ -0,0 +1,204 @@
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"
"os"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 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(delivery.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(delivery.ComposedNodeKey(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 := delivery.ManifestDigest([]byte(raw))
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
s.write(t, ManifestsRead, mustKey(delivery.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(delivery.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)
}
@@ -1,14 +1,16 @@
package main
// The seat's holder (novox/hq ADR 0570 clauses 1 to 5, 7; issue 1954 H1-H3): mesh-provisioner composes every placed
// module's provisioning plan and every provision from bus state, on change events and never on a timer, and keeps them
// in its states `plans` (`<node>.<module>`) and `provisions` (`<node>.<consumer>.<provision>[.<local>]`), which it alone
// writes.
// The seat's holder (novox/hq ADR 0570 clauses 1 to 5, 7, as changed by ADR 0593 §8; issue 1954 H1-H3): mesh-provisioner
// makes every placed module's provisioning plan and every provision from bus state, on change events and never on a
// timer, and keeps them in its states `plans` (`<node>.<module>`) and `provisions`
// (`<node>.<consumer>.<provision>[.<local>]`), which it alone writes. It composes nothing: each plan is the one the
// node-engine's published composition carries (`composed-<node>`, ADR 0593 §6), the node-engine alone composing.
//
// - **Composed per node.** A change of a node's placement or of one of its wants composes that node; a change of the
// indexer's module composes the nodes the module is placed on. Each node is composed twice: at the builds its wants
// name (its plans, what its client applies) and at the newest build of each placed module, so a build that adds a
// requirement has its key before its want (clause 2). A provision is needed while either needs it (clause 5).
// - **Made per node.** A change of a node's placement, of one of its wants or of one of its published compositions
// makes that node's plans again; a change of the indexer's module does it for the nodes the module is placed on.
// Each node's plans are read twice: at the builds its wants name (its plans, what its client applies) and at the
// newest build of each placed module, so a build that adds a requirement has its key before its want (clause 2).
// A provision is needed while either needs it (clause 5).
// - **A key's state comes from its provider's outcome alone** (clause 3, 7): the provider's own entry for that
// consumer, `<provider>_bound` under the same key: present is provided (the vault's generation carried into the
// reference, clause 6), refused is failed with why, waits is waits with what. The holder never sets provided by
@@ -32,8 +34,6 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/compose/fromruntime"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
@@ -54,15 +54,15 @@ type Refusal struct {
Since time.Time `json:"since"`
}
// The reads it composes from.
// The reads it makes the plans from. ComposedRead is every node's published compositions, `composed-<node>` (ADR 0593
// §6), read as the node-engine's named state: its keys are `<node>.<module>.<fingerprint hex>` and `<node>.node`.
const (
PlacementRead = delivery.PlacementRead
WantsRead = delivery.AssignerModule + ".wants"
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
BuildsRead = delivery.BuilderModule + ".builds"
HaveRead = "node-engine.have"
ResolvedRead = "mesh-config.resolved"
ComposedRead = "mesh-host.composed"
)
// BoundReads are the providers' entries this module's manifest reads, `<module>.bound`, whose changes it hears. Which
@@ -75,17 +75,29 @@ var BoundReads = []string{"gitea", "keycloak", "postgres", "route-adapter", "rou
// boundRead is a provider's entries as this module reads them.
func boundRead(module string) string { return module + ".bound" }
// Composer is a node's provisioning plans, composed from the bus over one read: at the builds keptAt names (module →
// fingerprint, the newest build of every other placed module), and at the newest build of each placed module (issue
// 1954: once, not twice). A module whose build or plan refuses is held (frombus.PlansHeld; issue 774, one bad module
// must not drop its node): in held with why, none of its plan in plans; a scope's Err is a refusal that is no single
// module's.
// Composer is a node's provisioning plans: at the builds keptAt names (module → fingerprint, the newest build of every
// other placed module), and at the newest build of each placed module. A module whose composition is refused, or not
// published yet, is held (issue 774, one bad module must not drop its node): in held with why, none of its plan in
// plans; a scope's Err is a failure that is no single module's.
type Composer func(ctx context.Context, node string, keptAt map[string]string) Composition
// Composition is a node's plans at its wanted builds and at its newest, and where the time went.
type Composition struct {
AtWants, Newest frombus.HeldPlans
Took []frombus.Phase
AtWants, Newest HeldPlans
Took []Phase
}
// HeldPlans is one scope's plans by module, the modules held with why, and a failure that is no single module's.
type HeldPlans struct {
Plans map[string]catalogue.Plan
Held map[string]string
Err error
}
// Phase is one step of reading a node's plans and how long it took.
type Phase struct {
Name string
Took time.Duration
}
// Asker is the operator's channel as this module asks on it: publish an ask under its own name, and read the router's
@@ -1106,25 +1118,139 @@ func orSaid(words ...string) string {
return ""
}
// FromWatched is the holder's composer over one watched copy of its reads (novox/hq ADR 0580 clause 2): frombus's plans
// of a node, at the builds keptAt names and at the newest over one read of the copy (placement, haves, modules,
// resolved values), a build by key and a manifest once per digest; no state is read key by key per composition. A plan
// names no value, so no own secret and no credential is read (compose.Planning).
func FromWatched(ws *stdio.WatchedStates) Composer {
return composerOver(fromruntime.WatchedReader(ws))
}
// composerOver is the holder's composer over a reader: both scopes over one read (frombus.PlansHeldTwice).
func composerOver(r frombus.Reader) Composer {
return func(ctx context.Context, node string, keptAt map[string]string) Composition {
at, newest, took := frombus.PlansHeldTwice(ctx, r, frombus.Mesh{}, frombus.Scope{Node: node, KeptAt: keptAt})
return Composition{AtWants: at, Newest: newest, Took: took}
// FromCompositions is the holder's composer over the node-engine's published compositions (ADR 0593 §8): it composes
// nothing, and takes each module's plan from the key of `composed-<node>` at the build it is read at. At the wanted
// builds a module is read at the fingerprint keptAt names, else at the indexer's newest (FP2); at the newest, each placed
// module at FP2. A key whose engine refused the module holds it with the engine's words; a key not published yet holds
// it at its wanted build (its last plan kept, as for a refusal) and leaves it out of the newest, whose build the engine
// writes once it is done (ADR 0593 §6). A plan absent from a key is a module that hands the provisioner nothing.
func FromCompositions(s Store) Composer {
return func(_ context.Context, node string, keptAt map[string]string) Composition {
start := time.Now()
at, newest := HeldPlans{Plans: map[string]catalogue.Plan{}, Held: map[string]string{}},
HeldPlans{Plans: map[string]catalogue.Plan{}, Held: map[string]string{}}
published, err := publishedOn(s, node)
if err != nil {
at.Err, newest.Err = err, err
return Composition{AtWants: at, Newest: newest, Took: []Phase{{Name: "compositions read", Took: time.Since(start)}}}
}
placed, fp2 := placedModules(s, node)
atWants := map[string]string{}
for _, m := range placed {
atWants[m] = fp2[m]
}
for m, fp := range keptAt {
atWants[m] = fp
}
for _, m := range sortedKeys(atWants) {
fp := atWants[m]
if fp == "" {
continue
}
c, ok := published[composedAt(node, m, fp)]
switch {
case !ok:
at.Held[m] = fmt.Sprintf("the composition of %s at %s is not published by the node-engine of %s yet", m, fp, node)
case c.Refused != "":
at.Held[m] = c.Refused
case c.Plan != nil:
at.Plans[m] = *c.Plan
}
}
for _, m := range placed {
if fp2[m] == "" {
continue
}
c, ok := published[composedAt(node, m, fp2[m])]
switch {
case !ok:
case c.Refused != "":
newest.Held[m] = c.Refused
case c.Plan != nil:
newest.Plans[m] = *c.Plan
}
}
return Composition{AtWants: at, Newest: newest, Took: []Phase{{Name: "compositions read", Took: time.Since(start)}}}
}
}
// WatchedReads are the states the holder reads whole, each from the watched copy: what the composer reads and the
// wants (ADR 0580 clause 2), and the providers' entries, whose watch is also what rejudges a key.
var WatchedReads = append([]string{PlacementRead, WantsRead, ModulesRead, HaveRead, ResolvedRead}, boundReads()...)
// publishedPlan is what the holder reads of a published composition (delivery.Composed): its plan, or the engine's
// refusal. Read leniently, so a field the node-engine adds later is no failure here.
type publishedPlan struct {
Plan *catalogue.Plan `json:"plan"`
Refused string `json:"refused"`
}
// composedAt is a module's key in its node's compositions, or "" for a fingerprint no key can name.
func composedAt(node, module, fp string) string {
k, err := delivery.ComposedKey(node, module, fp)
if err != nil {
return ""
}
return k
}
// publishedOn is every module composition the node's engine publishes, by key.
func publishedOn(s Store, node string) (map[string]publishedPlan, error) {
keys, err := s.Keys(ComposedRead)
if err != nil {
return nil, fmt.Errorf("the compositions the node-engines publish cannot be read: %w", err)
}
out := map[string]publishedPlan{}
for _, k := range keys {
n, _, _, isNode, err := delivery.ParseComposedKey(k)
if err != nil || isNode || n != node {
continue
}
e, err := s.Get(ComposedRead, k)
if err != nil || e == nil {
continue
}
var c publishedPlan
if err := json.Unmarshal(e.Value, &c); err != nil {
c.Refused = fmt.Sprintf("its published composition %s does not read: %v", k, err)
}
out[k] = c
}
return out, nil
}
// placedModules is every module placed on node, sorted, and the indexer's newest fingerprint (FP2) of each.
func placedModules(s Store, node string) ([]string, map[string]string) {
raw := map[string][]byte{}
if keys, err := s.Keys(PlacementRead); err == nil {
for _, k := range keys {
if n, _, _, err := delivery.ParsePlacementKey(k); err != nil || n != node {
continue
}
if e, err := s.Get(PlacementRead, k); err == nil && e != nil {
raw[k] = e.Value
}
}
}
placements, _ := delivery.PlacementsOf(raw)
var placed []string
fp2 := map[string]string{}
for _, m := range placements[node].Modules {
placed = append(placed, m.Module)
mk, err := delivery.ModulesKey(m.Module)
if err != nil {
continue
}
if e, err := s.Get(ModulesRead, mk); err == nil && e != nil {
if mod, err := delivery.DecodeModule(e.Value); err == nil {
fp2[m.Module] = mod.FP2
}
}
}
sort.Strings(placed)
return placed, fp2
}
// WatchedReads are the states the holder reads whole, each from the watched copy (ADR 0580 clause 2): the placement,
// the wants, the indexer's modules, the haves and the published compositions, and the providers' entries, whose watch
// is also what rejudges a key.
var WatchedReads = append([]string{PlacementRead, WantsRead, ModulesRead, HaveRead, ComposedRead}, boundReads()...)
// OwnStates are the states the holder alone writes (ADR 0570 clause 1), read from the watched copy too (issue 1954: some
// 250 to 400 reads by key per composition): each of its own writes is taken into the copy at once (WatchedStates.Wrote),
@@ -1206,8 +1332,9 @@ func (w watchingStore) Delete(state, key string) error {
return w.ws.Removed(state, key)
}
// NodesOfNodeKey is the node a key that starts with it names: a resolved value's `<node>.<module>.<key>`, a have's
// `<node>.<module>` or `<node>.node`. A change of either recomposes that node (ADR 0580 clause 2).
// NodesOfNodeKey is the node a key that starts with it names: a have's `<node>.<module>` or `<node>.node`, a published
// composition's `<node>.<module>.<fingerprint hex>` or `<node>.node`. A change of either makes that node's plans
// again (ADR 0580 clause 2, ADR 0593 §8).
func NodesOfNodeKey(key string) []string {
if n, _, ok := strings.Cut(key, "."); ok && n != "" {
return []string{n}
@@ -2,12 +2,13 @@
package main
// The holder against a real bus (novox/hq ADR 0570 clauses 1-3, 5; issue 1954 H5): its states and every input it reads
// are a real nats-server's key-value buckets, its plans composed by mesh-sdk's composition from the watched copy of those
// buckets (ADR 0580 clause 2) filled with the mesh frombustest gives (restic on n1, requiring postgres-database from
// postgres there); no input is read key by key per composition. A placement and a
// want compose the key; the providers' outcomes (the vault's entry for the credential, postgres's own have) turn it
// provided; restic leaving the node makes it retiring and raises the ask.
// The holder against a real bus (novox/hq ADR 0570 clauses 1-3, 5, as changed by ADR 0593 §8; issue 1954 H5): its
// states and every input it reads are a real nats-server's key-value buckets, read through the watched copy of those
// buckets (ADR 0580 clause 2); the plans come from n1's published compositions in `composed-n1` (ADR 0593 §6), as
// captured from the composition of mesh-sdk's frombustest mesh (restic on n1, requiring postgres-database from postgres
// there), and nothing is composed. A placement, a want and a published composition make the key; the providers'
// outcomes (the vault's entry for the credential, postgres's own have) turn it provided; restic leaving the node makes
// it retiring and raises the ask.
import (
"context"
@@ -22,9 +23,6 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/compose/frombus/frombustest"
"git.novox.be/novox/mesh-sdk/go/compose/fromruntime"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/testbus"
@@ -37,6 +35,9 @@ type kvStore struct {
}
func bucketOf(state string) string {
if state == ComposedRead { // the node-engine's named state: here n1's bucket, the one node that composes
return delivery.ComposedBucketPrefix + "n1"
}
if owner, name, ok := strings.Cut(state, "."); ok {
return owner + "_" + name
}
@@ -109,7 +110,7 @@ func (a *asked) Publish(_ string, body any, id string) error {
func (a *asked) Record(string) ([]byte, bool, error) { return nil, false, nil }
// 1954/H5.
func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) {
func TestTheHolderPlansProvidesAndAsksOnTheBus(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
nc, err := nats.Connect(testbus.URL(t))
@@ -122,85 +123,26 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
s := kvStore{ctx: ctx, js: js}
mesh, err := frombustest.Provisioned()
if err != nil {
t.Fatal(err)
}
node := frombustest.Node
// The bus as the holder reads it: the indexer's modules and manifests, a placement naming where the vault runs, the
// want of restic on n1.
for module, m := range mesh.Module {
m.V = 1
raw, _ := json.Marshal(m)
if err := s.Put(ModulesRead, module, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
for digest, raw := range mesh.Manifests {
var named struct {
Module string `json:"module"`
}
_ = json.Unmarshal(raw, &named)
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: named.Module, JSON: string(raw)})
key, _ := delivery.ManifestsKey(digest)
if err := s.Put(ManifestsRead, key, json.RawMessage(value)); err != nil {
t.Fatal(err)
}
}
// The vault keeps its outcomes as entries: its manifest declares `bound`.
vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}`
var c today
readJSON(t, "testdata/today-composed.json", &c)
node := c.Node
// The bus as the holder reads it: the indexer's modules and manifests, n1's placement and the vault's on n2, the
// want of restic on n1, and what n1's engine published.
mem := goldenMesh(t, c)
published(t, mem, c)
vaultManifest := `{"module":"mesh-vault","version":"1","state":["bound"]}` // the vault keeps its outcomes as entries
vaultDigest := delivery.ManifestDigest([]byte(vaultManifest))
vaultValue, _ := delivery.EncodeManifest(delivery.Manifest{Module: provisions.VaultModule, JSON: vaultManifest})
vaultKey, _ := delivery.ManifestsKey(vaultDigest)
if err := s.Put(ManifestsRead, vaultKey, json.RawMessage(vaultValue)); err != nil {
t.Fatal(err)
}
if err := s.Put(ModulesRead, provisions.VaultModule, map[string]any{"v": 1, "none2": "the test's", "manifest": vaultDigest,
"repository": "novox/mesh-catalog", "commit": "c1"}); err != nil {
t.Fatal(err)
}
vault, _ := json.Marshal(map[string]any{"v": 1, "node": "n2", "decided": map[string]any{},
"modules": []map[string]any{{"module": provisions.VaultModule}}})
if err := s.Put(PlacementRead, "placement.n2", json.RawMessage(vault)); err != nil {
t.Fatal(err)
}
fp := frombustest.FPs()["restic"]
if err := s.Put(WantsRead, "want."+node+".restic", map[string]any{"v": 1, "fp": fp, "self": true}); err != nil {
t.Fatal(err)
}
// The rest of the mesh on the bus as its writers keep it: n1's placement, its record in its have, the builds.
putPlacement := func() {
raw, err := delivery.EncodePlacement(mesh.Placement[node])
if err != nil {
t.Fatal(err)
}
if err := s.Put(PlacementRead, "placement."+node, json.RawMessage(raw)); err != nil {
t.Fatal(err)
mem.write(t, ManifestsRead, mustKey(delivery.ManifestsKey(vaultDigest)), string(vaultValue))
mem.write(t, ModulesRead, provisions.VaultModule, `{"v":1,"none2":"the test's","manifest":"`+vaultDigest+`"}`)
for state, keys := range mem.states {
for key, raw := range keys {
if err := s.Put(state, key, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
}
putPlacement()
for _, n := range []string{node, "n2"} { // n2, where the vault runs, has its record too
rec := mesh.Record[node]
rec.Node = n
recRaw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &rec})
if err != nil {
t.Fatal(err)
}
if err := s.Put(HaveRead, mustKey(delivery.HaveNodeKey(n)), json.RawMessage(recRaw)); err != nil {
t.Fatal(err)
}
}
for at, b := range mesh.Builds {
module, fp, _ := strings.Cut(at, " ")
raw, err := delivery.EncodeBuilderBuild(*b)
if err != nil {
t.Fatal(err)
}
if err := s.Put(BuildsRead, mustKey(delivery.BuilderBuildKey(module, fp)), json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
// The watched copy over the real buckets: each state by one watch, a build and a manifest by key.
// The watched copy over the real buckets: each state by one watch, a manifest by key.
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
kv, err := s.kv(state)
if err != nil {
@@ -250,18 +192,9 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) {
}
}
}
fetch := func(_ context.Context, url string) ([]byte, error) {
for digest, body := range mesh.Archives {
if strings.HasSuffix(url, digest) {
return body, nil
}
}
return nil, errors.New("no archive at " + url)
}
reader := frombus.StatesReader(fromruntime.Watched{WatchedStates: ws}, fetch)
a := &asked{}
h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf,
Compose: composerOver(reader)}
h := &Holder{Store: Watching(s, ws), Asks: a, Now: time.Now, Logf: t.Logf}
h.Compose = FromCompositions(h.Store)
state := func(key string) provisions.Provision {
e, _ := s.Get(ProvisionsState, key)
if e == nil {
@@ -286,7 +219,7 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
}
if err := s.Put(HaveRead, node+".postgres", map[string]any{"v": 1, "fp": frombustest.FPs()["postgres"], "phase": "running",
if err := s.Put(HaveRead, node+".postgres", map[string]any{"v": 1, "fp": fpsOf["postgres"], "phase": "running",
"health": "ok"}); err != nil {
t.Fatal(err)
}
@@ -299,17 +232,11 @@ func TestTheHolderComposesProvidesAndAsksOnTheBus(t *testing.T) {
if p := state(key); p.State != provisions.Provided {
t.Fatalf("the outcomes did not turn it provided: %+v", p)
}
// restic leaves the node and its want goes: retiring, one ask.
p := mesh.Placement[node]
var kept []delivery.PlacedModule
for _, m := range p.Modules {
if m.Module != "restic" {
kept = append(kept, m)
}
// restic leaves the node and its want goes: retiring, one ask. Its compositions stay until its engine deletes them.
if err := s.Put(PlacementRead, "placement."+node, json.RawMessage(`{"v":1,"node":"`+node+`","decided":{},`+
`"modules":[{"module":"store"},{"module":"postgres"},{"module":"mesh-provisioner-client"}]}`)); err != nil {
t.Fatal(err)
}
p.Modules = kept
mesh.Placement[node] = p
putPlacement()
if err := s.Delete(WantsRead, "want."+node+".restic"); err != nil {
t.Fatal(err)
}
@@ -15,7 +15,6 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
@@ -71,8 +70,8 @@ func newMesh() *mesh {
Now: func() time.Time { now = now.Add(time.Second); return now },
Compose: func(_ context.Context, node string, keptAt map[string]string) Composition {
m.composed = append(m.composed, node)
return Composition{AtWants: frombus.HeldPlans{Plans: m.atWant[node], Held: m.heldAt[node]},
Newest: frombus.HeldPlans{Plans: m.atNewest[node]}}
return Composition{AtWants: HeldPlans{Plans: m.atWant[node], Held: m.heldAt[node]},
Newest: HeldPlans{Plans: m.atNewest[node]}}
}}
// What each provider's manifest declares: postgres, keycloak and the vault keep entries, the others report by their
// have.
@@ -386,10 +385,10 @@ func TestAModuleHeldWithoutAWantIsComposedAtTheBuildItRuns(t *testing.T) {
var keptAt map[string]string
m.h.Compose = func(_ context.Context, node string, at map[string]string) Composition {
keptAt = at
return Composition{AtWants: frombus.HeldPlans{Plans: map[string]catalogue.Plan{"nats": {}}}}
return Composition{AtWants: HeldPlans{Plans: map[string]catalogue.Plan{"nats": {}}}}
}
// ADR 0580 clause 2: the reads come from one watched copy, each state watched once and none read key by key from the
// bus, and a change the copy takes wakes the node it touches (a resolved value its node).
// bus, and a change the copy takes wakes the node it touches (a published composition its node).
bus := &readsOf{Store: m.s, read: map[string]int{}}
handles, watches := map[string]func(stdio.StateChange) error{}, map[string]int{}
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
@@ -404,7 +403,7 @@ func TestAModuleHeldWithoutAWantIsComposedAtTheBuildItRuns(t *testing.T) {
}, func(string, string) ([]byte, bool, error) { return nil, false, nil })
m.h.Store = Watching(bus, ws)
wk := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)}
for _, r := range []string{ResolvedRead, HaveRead} {
for _, r := range []string{ComposedRead, HaveRead} {
ws.Follow(r, func(c stdio.StateChange) error { wk.add(NodesOfNodeKey(c.Key)...); return nil })
}
for i := range 2 {
@@ -429,12 +428,14 @@ func TestAModuleHeldWithoutAWantIsComposedAtTheBuildItRuns(t *testing.T) {
t.Errorf("%s was read %d times from the bus and watched %d times", r, bus.read[r], watches[r])
}
}
// The current haves queued their nodes when watched; a later change of a resolved value or a have queues its node.
// The current haves queued their nodes when watched; a later change of a published composition or a have
// queues its node.
wk.pending = map[string]bool{}
_ = handles[ResolvedRead](stdio.StateChange{Key: "g14.dunst.font-size", Op: stdio.OpPut, Value: []byte("13")})
_ = handles[ComposedRead](stdio.StateChange{Key: "g14.dunst." + strings.Repeat("ee", 32), Op: stdio.OpPut,
Value: []byte("{}")})
_ = handles[HaveRead](stdio.StateChange{Key: "ace.restic", Op: stdio.OpDelete})
if !wk.pending["g14"] || !wk.pending["ace"] || len(wk.pending) != 2 {
t.Errorf("a resolved value's and a have's change queued %v, want g14 and ace", wk.pending)
t.Errorf("a published composition's and a have's change queued %v, want g14 and ace", wk.pending)
}
}
@@ -1,7 +1,7 @@
package main
// The holder's tools (novox/hq ADR 0570 Consequences, ADR 0564 §12's rule): a read of every key of `provisions` and
// `plans`, one key whole, each state's fill, composing one node now, and reading the operator's answers now.
// `plans`, one key whole, each state's fill, making one node's plans now, and reading the operator's answers now.
import (
"context"
@@ -36,9 +36,10 @@ func holderTools(h *Holder) []stdio.Tool {
return h.read(ProvisionsState, arg(a, "key"), arg(a, "node"), arg(a, "state"))
}},
{Name: PlansTool,
Description: "Every module's provisioning plan the seat composed, <node>.<module>, narrowed by node, with how many " +
"keys and bytes the state holds, and each placed module held with no plan composed and why (refused: its " +
"manifest or its plan refuses, the node's other modules composed); or one plan whole.",
Description: "Every module's provisioning plan the seat keeps, <node>.<module>, as the node-engine's published " +
"composition carries it (composed-<node>), narrowed by node, with how many keys and bytes the state holds, " +
"and each placed module held with its last plan kept and why (refused: the node-engine refused its " +
"composition, or has not published it at the wanted build yet); or one plan whole.",
Input: map[string]any{"type": "object", "properties": map[string]any{"key": str("one plan, <node>.<module>"),
"node": str("a node")}},
Run: func(a map[string]any) (any, error) {
@@ -54,8 +55,9 @@ func holderTools(h *Holder) []stdio.Tool {
return out, nil
}},
{Name: ComposeTool,
Description: "Compose one node's plans and provisions now from the bus, as a change event would, and say what " +
"was written, removed, changed and asked.",
Description: "Make one node's plans and provisions now from the node-engine's published compositions and the " +
"providers' outcomes, as a change event would (it composes nothing), and say what was written, removed, " +
"changed and asked, and where the time went.",
Input: map[string]any{"type": "object", "properties": map[string]any{"node": str("the node")},
"required": []string{"node"}},
Run: func(a map[string]any) (any, error) {
@@ -1,7 +1,7 @@
// mesh-provisioner: the provisioner's server, holder of the mesh-provisioner seat once for the mesh (novox/hq ADR
// 0349 clause 3, ADR 0570). A Go bundle served by a runtime of its own, on its own bus account (`runs-as`, ADR 0259
// §8), keeping mesh-provisioner_provisions and _plans (every provision and every plan, composed from the bus on change
// events, its alone), _registry (which domain answers which kind of placeholder, its alone), and _applied and
// §8), keeping mesh-provisioner_provisions and _plans (every provision and every plan, made from the node-engines'
// published compositions on change events, its alone; novox/hq ADR 0593 §8), _registry (which domain answers which kind of placeholder, its alone), and _applied and
// _collect, which each machine's provisioner client writes for its own machine; _needs is retired and emptied at its
// start. stdout is the MCP channel; what this module says, it says on stderr.
package main
@@ -125,14 +125,15 @@ func slowest(took []Took) time.Duration {
func main() {
s := runtimeStore{}
ws := stdio.NewWatchedStates() // one copy of what it reads, each state by its watch (ADR 0580 clause 2)
h := &Holder{Store: Watching(s, ws), Compose: FromWatched(ws), Asks: runtimeAsks{}, Now: time.Now, Logf: logf}
h := &Holder{Store: Watching(s, ws), Asks: runtimeAsks{}, Now: time.Now, Logf: logf}
h.Compose = FromCompositions(h.Store) // the node-engines' published compositions, from the copy (ADR 0593 §8)
w := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)}
go w.run(h)
go starting(h)
// Each watch's handler runs after the copy took its change, so a composition it wakes reads the copy that change made.
go ws.Follow(PlacementRead, func(c stdio.StateChange) error { w.add(NodesOfPlacement(c.Key)...); return nil })
go ws.Follow(WantsRead, func(c stdio.StateChange) error { w.add(NodesOfWant(c.Key)...); return nil })
go ws.Follow(ResolvedRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
go ws.Follow(ComposedRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
go ws.Follow(HaveRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
go ws.Follow(ModulesRead, func(c stdio.StateChange) error {
if module, err := delivery.ParseModulesKey(c.Key); err == nil {
@@ -9,7 +9,7 @@ import (
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// memStore is the bus as mesh-provisioner reaches it, in memory: the states it keeps.
@@ -326,9 +326,15 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
t.Errorf("the manifest lists %s, which is not served", tool)
}
}
for _, r := range append([]string{WantsRead}, frombus.StatesReads...) { // what fromruntime.Reader reads
for _, r := range append(slices.Clone(WatchedReads), ManifestsRead) {
if !slices.Contains(m.Reads, r) {
t.Errorf("the manifest does not read %s, which the composition reads", r)
t.Errorf("the manifest does not read %s, which the holder reads", r)
}
}
// It composes nothing (ADR 0593 §8): no build, and no config value, is read to make a plan.
for _, r := range []string{delivery.BuilderModule + ".builds", "mesh-config.resolved"} {
if slices.Contains(m.Reads, r) {
t.Errorf("the manifest reads %s, which only a composer reads", r)
}
}
for _, p := range BoundReads {
@@ -0,0 +1,100 @@
{
"node": "n1",
"kept-at": {
"restic": "src2:2222222222222222222222222222222222222222222222222222222222222222"
},
"at-wants": {
"Plans": {
"restic": {
"templates": {
"source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e",
"digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e"
},
"files": [
{
"id": "declared",
"path": "/var/lib/restic/backups.conf",
"template": "backups.conf",
"owner-at-runtime": "owned"
},
{
"id": "data-declared",
"path": "/var/lib/restic/data.conf",
"template": "data.conf",
"owner-at-runtime": "owned"
},
{
"id": "retention",
"path": "/var/lib/restic/retention.conf",
"template": "retention.conf",
"owner-at-runtime": "owned"
}
],
"placement": {
"backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n",
"backup:lines": ""
},
"secrets": [
{
"name": "repository",
"path": "/var/lib/restic/repository.secret"
}
]
}
},
"Held": {},
"Err": null
},
"newest": {
"Plans": {
"restic": {
"templates": {
"source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e",
"digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e"
},
"files": [
{
"id": "declared",
"path": "/var/lib/restic/backups.conf",
"template": "backups.conf",
"owner-at-runtime": "owned"
},
{
"id": "data-declared",
"path": "/var/lib/restic/data.conf",
"template": "data.conf",
"owner-at-runtime": "owned"
},
{
"id": "retention",
"path": "/var/lib/restic/retention.conf",
"template": "retention.conf",
"owner-at-runtime": "owned"
}
],
"placement": {
"backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n",
"backup:lines": ""
},
"secrets": [
{
"name": "repository",
"path": "/var/lib/restic/repository.secret"
}
],
"provisions": [
{
"provision": "postgres-database",
"provider": "n1",
"provider-module": "postgres",
"at": "127.0.0.1",
"from": "n1",
"as": "mesh_n1_restic"
}
]
}
},
"Held": {},
"Err": null
}
}
@@ -0,0 +1,71 @@
{
"plans": {
"n1.restic": {
"v": 1,
"node": "n1",
"module": "restic",
"plan": {
"templates": {
"source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e",
"digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e"
},
"files": [
{
"id": "declared",
"path": "/var/lib/restic/backups.conf",
"template": "backups.conf",
"owner-at-runtime": "owned"
},
{
"id": "data-declared",
"path": "/var/lib/restic/data.conf",
"template": "data.conf",
"owner-at-runtime": "owned"
},
{
"id": "retention",
"path": "/var/lib/restic/retention.conf",
"template": "retention.conf",
"owner-at-runtime": "owned"
}
],
"placement": {
"backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n",
"backup:lines": ""
},
"secrets": [
{
"name": "repository",
"path": "/var/lib/restic/repository.secret"
}
]
}
}
},
"provisions": {
"n1.restic.postgres-database": {
"v": 1,
"provision": "postgres-database",
"consumer": {
"module": "restic",
"node": "n1"
},
"provider": {
"module": "postgres",
"node": "n1"
},
"state": "asked",
"why": "no plan names it yet: the newest build of restic requires it and its wanted build does not, so its provider binds it from this key's plan entry; waits for postgres on n1 to run: it says nothing yet",
"plan": {
"provision": "postgres-database",
"provider": "n1",
"provider-module": "postgres",
"at": "127.0.0.1",
"from": "n1",
"as": "mesh_n1_restic"
},
"at": "2026-11-02T18:00:00Z"
}
},
"refused": {}
}
@@ -0,0 +1,110 @@
{
"node": "n1",
"kept-at": {
"restic": "src2:3333333333333333333333333333333333333333333333333333333333333333"
},
"at-wants": {
"Plans": {
"restic": {
"templates": {
"source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e",
"digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e"
},
"files": [
{
"id": "declared",
"path": "/var/lib/restic/backups.conf",
"template": "backups.conf",
"owner-at-runtime": "owned"
},
{
"id": "data-declared",
"path": "/var/lib/restic/data.conf",
"template": "data.conf",
"owner-at-runtime": "owned"
},
{
"id": "retention",
"path": "/var/lib/restic/retention.conf",
"template": "retention.conf",
"owner-at-runtime": "owned"
}
],
"placement": {
"backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n",
"backup:lines": ""
},
"secrets": [
{
"name": "repository",
"path": "/var/lib/restic/repository.secret"
}
],
"provisions": [
{
"provision": "postgres-database",
"provider": "n1",
"provider-module": "postgres",
"at": "127.0.0.1",
"from": "n1",
"as": "mesh_n1_restic"
}
]
}
},
"Held": {},
"Err": null
},
"newest": {
"Plans": {
"restic": {
"templates": {
"source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e",
"digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e"
},
"files": [
{
"id": "declared",
"path": "/var/lib/restic/backups.conf",
"template": "backups.conf",
"owner-at-runtime": "owned"
},
{
"id": "data-declared",
"path": "/var/lib/restic/data.conf",
"template": "data.conf",
"owner-at-runtime": "owned"
},
{
"id": "retention",
"path": "/var/lib/restic/retention.conf",
"template": "retention.conf",
"owner-at-runtime": "owned"
}
],
"placement": {
"backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n",
"backup:lines": ""
},
"secrets": [
{
"name": "repository",
"path": "/var/lib/restic/repository.secret"
}
],
"provisions": [
{
"provision": "postgres-database",
"provider": "n1",
"provider-module": "postgres",
"at": "127.0.0.1",
"from": "n1",
"as": "mesh_n1_restic"
}
]
}
},
"Held": {},
"Err": null
}
}
@@ -0,0 +1,73 @@
{
"plans": {
"n1.restic": {
"v": 1,
"node": "n1",
"module": "restic",
"plan": {
"templates": {
"source": "http://127.0.0.1:5000/v2/restic/files/blobs/e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e",
"digest": "e30dc95f314f1a9f5365deef50f14f7a36ccaaaa34038621bbfa3a13509fca2e"
},
"files": [
{
"id": "declared",
"path": "/var/lib/restic/backups.conf",
"template": "backups.conf",
"owner-at-runtime": "owned"
},
{
"id": "data-declared",
"path": "/var/lib/restic/data.conf",
"template": "data.conf",
"owner-at-runtime": "owned"
},
{
"id": "retention",
"path": "/var/lib/restic/retention.conf",
"template": "retention.conf",
"owner-at-runtime": "owned"
}
],
"placement": {
"backup:data": "# restic\nitem repository rebuildable /var/lib/restic/repository - none walk\nitem state cache /var/lib/restic - none walk\n",
"backup:lines": ""
},
"secrets": [
{
"name": "repository",
"path": "/var/lib/restic/repository.secret"
}
],
"provisions": [
{
"provision": "postgres-database",
"provider": "n1",
"provider-module": "postgres",
"at": "127.0.0.1",
"from": "n1",
"as": "mesh_n1_restic"
}
]
}
}
},
"provisions": {
"n1.restic.postgres-database": {
"v": 1,
"provision": "postgres-database",
"consumer": {
"module": "restic",
"node": "n1"
},
"provider": {
"module": "postgres",
"node": "n1"
},
"state": "asked",
"why": "waits for postgres on n1 to run: it says nothing yet",
"at": "2026-11-02T18:00:00Z"
}
},
"refused": {}
}
+1 -2
View File
@@ -19,9 +19,8 @@
"mesh-delivery-assigner.wants",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests",
"mesh-delivery-builder.builds",
"node-engine.have",
"mesh-config.resolved",
"mesh-host.composed",
"gitea.bound",
"keycloak.bound",
"postgres.bound",