Merge the provisioner's move onto the node-engine's composition into the sdk1 release
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -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 {
|
||||
|
||||
+100
@@ -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
|
||||
}
|
||||
}
|
||||
+71
@@ -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
|
||||
}
|
||||
}
|
||||
+73
@@ -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": {}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user