Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+33
View File
@@ -0,0 +1,33 @@
# mesh-provisioner
Holds the `mesh-provisioner` seat once for the mesh: the provisioner's server (novox/hq ADR 0349 clause 3, design 57
P4). Go, one binary (`cmd/mesh-provisioner`), served by a runtime of its own on its own bus account (`runs-as`), so
that its account alone writes the registry. The provisioner client on each node (`node-provisioner`, design 57 P5)
is the module `mesh-provisioner-client`.
## What it keeps
- `mesh-provisioner_registry`: which domain answers which kind of placeholder, keyed by the kind (`config`,
`secret`, `bound` — the provider's own bucket, `<provider-module>_bound`, keyed
`<node>.<module>.<provision>[.<local>]` (mesh-sdk `go/provider`) — `consumers` — a provider's consumers'
credentials, asked of the vault by the provider machine's client (novox/hq ADR 0373) — `collected` — and `shell`, `environment` and `contribution`, read as collections of the same
name (design 57 L1) — and the placement namespaces `dir`, `access`, `port`, `seat`, `machine`). Each
answer names the owning module and where a client finds the values: a domain's bucket and its key shape
(placement's is the controller's assignments mirror, whose value carries the provisioner's plan of a module handed
to it), or a seat's verb asked on request and reply (a secret is never in a bucket). Compiled in
`registry.go`, written at start and every hour, only where it differs; a kind no domain answers is deleted.
- `mesh-provisioner_needs`: what each module on each node needs, `<node>.<module>.<kind>.<name>`.
- `mesh-provisioner_applied`: what is applied where, `<node>.<module>`.
- `mesh-provisioner_collect`: the items of every collection, `<node>.<collection>.<contributor>`, the collection
with a character a key's token cannot hold written `=<hex>` (`shell=3Azsh=3Anormal` is `shell:zsh:normal`), and
read back by its name.
The last three are written by each node's provisioner client for its own node's keys alone: the controller grants
the client `<node>.>` of each and nothing wider (mesh-controller `internal/broker/provisioner.go`), and a provider
reads the needs without writing them. This module writes only the registry.
## The verb
`mesh-provisioner.provisioning` with `what` `registry` (the default), `needs`, `applied` or `collect`, narrowed by
`node` and `module` (for `collect`, the contributor) and, for `collect`, `collection`. Reads only. A key out of its
bucket's shape is said in the answer, never dropped quietly.
@@ -0,0 +1,95 @@
// mesh-provisioner: the provisioner's server, holder of the mesh-provisioner seat once for the mesh (novox/hq ADR
// 0349 clause 3, design 57 P4). A Go bundle served by a runtime of its own, on its own bus account (`runs-as`, ADR
// 0259 §8), keeping four buckets: mesh-provisioner_registry (which domain answers which kind of placeholder, its
// alone), and mesh-provisioner_needs, _applied and _collect, which each machine's provisioner client writes for its
// own machine (the controller grants the client those keys alone, issue 670). stdout is the MCP channel; what this
// module says, it says on stderr.
package main
import (
"fmt"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Seat is the role this module holds, once for the mesh, and Verb its one verb.
const (
Seat = "mesh-provisioner"
Verb = "provisioning"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
// runtimeStore is the bus, through the runtime that launched this bundle.
type runtimeStore struct{}
func (runtimeStore) Get(state, key string) (*Entry, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, err
}
return &Entry{Key: e.Key, Value: e.Value, Revision: e.Revision}, nil
}
func (runtimeStore) Put(state, key string, value any) error {
_, err := stdio.State(state).Put(key, value)
return err
}
func (runtimeStore) Delete(state, key string) error { return stdio.State(state).Delete(key) }
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func main() {
go publishing(runtimeStore{})
if err := stdio.Serve("", Tools(runtimeStore{})); err != nil {
logf("%v", err)
os.Exit(1)
}
}
// publishing writes the registry once the runtime answers, and again every hour, so a registry emptied by hand or
// a bucket made again is written back without a restart. The runtime answers only once Serve runs, so the first
// pass is tried again until it lands.
func publishing(s Store) {
for wait := 2 * time.Second; ; {
time.Sleep(wait)
written, deleted, err := Publish(s)
switch {
case err != nil:
logf("[mesh-provisioner] the registry is not written yet (%v); trying again in %s", err, wait)
wait = min(wait*2, time.Minute)
continue
case len(written) > 0 || len(deleted) > 0:
logf("[mesh-provisioner] the registry: wrote %v, deleted %v", written, deleted)
}
wait = time.Hour
}
}
// Tools is the seat's verb as the runtime serves it.
func Tools(s Store) []stdio.Tool {
return []stdio.Tool{{
Name: Seat + "." + Verb,
Description: "Read what the provisioner keeps (novox/hq ADR 0349 clause 3): the registry (which domain " +
"answers which kind of placeholder, the default), the needs each module on each node recorded, what is " +
"applied where, or the items of every collection, narrowed by node and module, and collect by " +
"collection. Reads only: each node's provisioner client writes its own node's needs, applied and items.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"what": map[string]any{"type": "string", "enum": []string{Registry, Needs, Applied, Collect},
"description": "registry when not given"},
"node": map[string]any{"type": "string", "description": "a node"},
"module": map[string]any{"type": "string", "description": "a module: for collect, a contributor"},
"collection": map[string]any{"type": "string", "description": "for collect: a collection"},
}},
Run: func(args map[string]any) (any, error) {
q, err := queryOf(args)
if err != nil {
return nil, err
}
return Do(s, q)
},
}}
}
@@ -0,0 +1,306 @@
package main
import (
"encoding/json"
"os"
"slices"
"sort"
"strings"
"sync"
"testing"
)
// memStore is the bus as mesh-provisioner reaches it, in memory: the states it keeps.
type memStore struct {
mu sync.Mutex
states map[string]map[string][]byte
puts int
}
func newStore() *memStore { return &memStore{states: map[string]map[string][]byte{}} }
func (m *memStore) Get(state, key string) (*Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.states[state][key]
if !ok {
return nil, nil
}
return &Entry{Key: key, Value: append([]byte(nil), v...)}, nil
}
func (m *memStore) Put(state, key string, value any) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
if m.states[state] == nil {
m.states[state] = map[string][]byte{}
}
m.states[state][key] = raw
m.puts++
return nil
}
func (m *memStore) Delete(state, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.states[state], key)
return nil
}
func (m *memStore) Keys(state string) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
var keys []string
for k := range m.states[state] {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, nil
}
// write is a client's write, as the bus lets it through for its own machine's keys.
func (m *memStore) write(t *testing.T, state, key, raw string) {
t.Helper()
if err := m.Put(state, key, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
// The registry answers every kind a moved module's template may name (ADR 0349 clause 2): config, secret, bound,
// collected and the placement namespaces, each with its domain and where a client finds it.
func TestTheRegistryAnswersEveryKindATemplateMayName(t *testing.T) {
var kinds []string
for _, a := range TheRegistry {
kinds = append(kinds, a.Kind)
if a.Domain == "" || a.Why == "" || !strings.HasPrefix(a.Decision, "novox/hq ADR ") {
t.Errorf("%s: an answer names its domain, why and its decision: %+v", a.Kind, a)
}
switch a.From {
case FromBucket:
if a.Bucket == "" || !strings.HasPrefix(a.Key, "<node>.") {
t.Errorf("%s: a value in a bucket names the bucket and a key with the node first: %+v", a.Kind, a)
}
if a.Ask != "" {
t.Errorf("%s: from a bucket, asked of nobody: %+v", a.Kind, a)
}
case FromAsk:
if a.Ask == "" || a.Bucket != "" {
t.Errorf("%s: a value asked for names its verb and no bucket: %+v", a.Kind, a)
}
default:
t.Errorf("%s: from %q", a.Kind, a.From)
}
}
want := []string{"config", "secret", "bound", "consumers", "collected", "shell", "environment", "contribution",
"dir", "access", "port", "seat", "machine", "backup"}
if !slices.Equal(kinds, want) {
t.Errorf("the registry's kinds are %v, want %v", kinds, want)
}
// The buckets a client is granted to read its node's keys of (mesh-controller internal/broker/provisioner.go):
// the config domain's resolved values, this module's own collections, and placement in each module's assignment,
// where the controller carries the provisioner's plan (issue 671): a client never reads the node's declaration.
for kind, bucket := range map[string]string{"config": "mesh-config_resolved", "collected": "mesh-provisioner_" + Collect,
"shell": "mesh-provisioner_" + Collect, "environment": "mesh-provisioner_" + Collect,
"contribution": "mesh-provisioner_" + Collect,
"dir": "mesh-controller_assignments", "access": "mesh-controller_assignments",
"port": "mesh-controller_assignments", "seat": "mesh-controller_assignments",
"machine": "mesh-controller_assignments", "backup": "mesh-controller_assignments"} {
i := slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == kind })
if TheRegistry[i].Bucket != bucket {
t.Errorf("%s is found in %s, want %s", kind, TheRegistry[i].Bucket, bucket)
}
}
// A secret is never in a bucket (ADR 0201 §13): asked of the vault.
i := slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == "secret" })
if a := TheRegistry[i]; a.From != FromAsk || a.Ask != "mesh-vault.get" {
t.Errorf("a secret is answered by %+v, want asked of mesh-vault.get", a)
}
// A consumer's values are in its provider's own bucket, one entry per consumer, keyed as the providers' helper
// writes it (mesh-sdk go/provider, issue 678): the consumer's machine first.
i = slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == "bound" })
if a := TheRegistry[i]; a.From != FromBucket || a.Bucket != "<provider-module>_bound" ||
a.Key != "<node>.<module>.<provision>[.<local>]" {
t.Errorf("a bound value is answered by %+v, want the provider's <provider-module>_bound, "+
"<node>.<module>.<provision>[.<local>]", a)
}
// A provider's consumers' credentials are the vault's, asked by the provider machine's client (ADR 0373).
i = slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == "consumers" })
if a := TheRegistry[i]; a.Domain != "mesh-vault" || a.From != FromAsk || a.Ask != "mesh-vault.get" ||
a.Decision != "novox/hq ADR 0373" {
t.Errorf("a provider's consumers are answered by %+v, want the vault, asked of mesh-vault.get (ADR 0373)", a)
}
}
// The registry is written whole at start, a pass that finds it equal writes nothing, a changed answer is written
// back, and a kind no domain answers any more is deleted.
func TestTheRegistryIsWrittenBackAndAQuietPassWritesNothing(t *testing.T) {
s := newStore()
written, deleted, err := Publish(s)
if err != nil {
t.Fatal(err)
}
if len(written) != len(TheRegistry) || len(deleted) != 0 {
t.Fatalf("the first pass wrote %v and deleted %v", written, deleted)
}
for _, a := range TheRegistry {
e, _ := s.Get(Registry, a.Kind)
var got Answer
if e == nil || json.Unmarshal(e.Value, &got) != nil || got != a {
t.Errorf("the registry's %s holds %v, want %+v", a.Kind, e, a)
}
}
puts := s.puts
if written, deleted, err := Publish(s); err != nil || len(written)+len(deleted) != 0 || s.puts != puts {
t.Fatalf("a quiet pass wrote %v, deleted %v (%v)", written, deleted, err)
}
s.write(t, Registry, "config", `{"kind":"config","domain":"mesh-controller"}`)
s.write(t, Registry, "setting", `{"kind":"setting"}`)
written, deleted, err = Publish(s)
if err != nil || !slices.Equal(written, []string{"config"}) || !slices.Equal(deleted, []string{"setting"}) {
t.Fatalf("a changed and an unknown kind: wrote %v, deleted %v (%v)", written, deleted, err)
}
}
func clientWrites(t *testing.T) *memStore {
s := newStore()
if _, _, err := Publish(s); err != nil {
t.Fatal(err)
}
s.write(t, Needs, "g14.dunst.config.font-size", `{"at":"2026-10-14T20:00:00Z"}`)
s.write(t, Needs, "g14.dunst.secret.token", `{}`)
s.write(t, Needs, "shanks.dunst.config.font-size", `{}`)
s.write(t, Needs, "shanks.gitea.bound.database.user", `{}`)
s.write(t, Applied, "g14.dunst", `{"build":"b1","outcome":"applied"}`)
s.write(t, Applied, "shanks.dunst", `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}`)
s.write(t, Collect, "g14.zsh-snippets.git", `{}`)
s.write(t, Collect, "g14.bar-blocks.disk-load", `{}`)
s.write(t, Collect, "g14.shell=3Azsh=3Anormal.mesh-cli", `{}`)
return s
}
func keysOf(r Read) []string {
var out []string
for _, row := range r.Rows {
out = append(out, row.Key)
}
return out
}
// Each bucket is read by its shape, its parts named, and narrowed by node, module and collection.
func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
s := clientWrites(t)
for _, c := range []struct {
q Query
want []string
}{
{Query{}, []string{"access", "backup", "bound", "collected", "config", "consumers", "contribution", "dir", "environment",
"machine", "port", "seat", "secret", "shell"}},
{Query{What: Needs}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token",
"shanks.dunst.config.font-size", "shanks.gitea.bound.database.user"}},
{Query{What: Needs, Node: "g14"}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token"}},
{Query{What: Needs, Module: "dunst", Node: "shanks"}, []string{"shanks.dunst.config.font-size"}},
{Query{What: Applied, Module: "dunst"}, []string{"g14.dunst", "shanks.dunst"}},
{Query{What: Applied, Node: "ace"}, nil},
{Query{What: Collect, Collection: "zsh-snippets"}, []string{"g14.zsh-snippets.git"}},
{Query{What: Collect, Module: "disk-load"}, []string{"g14.bar-blocks.disk-load"}},
// An old namespace's collection, by its name, not its key's token (design 57 L1).
{Query{What: Collect, Collection: "shell:zsh:normal"}, []string{"g14.shell=3Azsh=3Anormal.mesh-cli"}},
} {
r, err := Do(s, c.q)
if err != nil {
t.Fatalf("%+v: %v", c.q, err)
}
if got := keysOf(r); !slices.Equal(got, c.want) || r.Count != len(c.want) {
t.Errorf("%+v: read %v (%d), want %v", c.q, got, r.Count, c.want)
}
}
r, _ := Do(s, Query{What: Needs, Node: "shanks", Module: "gitea"})
if row := r.Rows[0]; row.Node != "shanks" || row.Module != "gitea" || row.Kind != "bound" || row.Name != "database.user" {
t.Errorf("a need's parts: %+v", row)
}
r, _ = Do(s, Query{What: Collect, Collection: "bar-blocks"})
if row := r.Rows[0]; row.Node != "g14" || row.Collection != "bar-blocks" || row.Module != "disk-load" {
t.Errorf("an item's parts: %+v", row)
}
r, _ = Do(s, Query{What: Applied, Node: "shanks"})
if string(r.Rows[0].Value) != `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}` {
t.Errorf("what is applied is read as the client wrote it: %s", r.Rows[0].Value)
}
}
// A key out of its bucket's shape is said, never dropped quietly.
func TestAKeyOutOfItsBucketsShapeIsSaid(t *testing.T) {
s := clientWrites(t)
s.write(t, Applied, "g14.dunst.extra", `{}`)
r, err := Do(s, Query{What: Applied})
if err != nil {
t.Fatal(err)
}
if r.Count != 2 || !strings.Contains(r.Said, "g14.dunst.extra") || !strings.Contains(r.Said, "<node>.<module>") {
t.Errorf("an unshaped key: %d rows, said %q", r.Count, r.Said)
}
}
// The verb's arguments are read strictly, and a narrowing that means nothing is refused rather than ignored.
func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
s := clientWrites(t)
run := Tools(s)[0].Run
if Tools(s)[0].Name != "mesh-provisioner.provisioning" {
t.Fatalf("the verb is served as %s", Tools(s)[0].Name)
}
for _, args := range []map[string]any{
{"what": "needs", "caller": "g14.dunst"},
{"node": 3},
{"what": "secrets"},
{"node": "g14"},
{"what": "needs", "collection": "zsh-snippets"},
} {
if _, err := run(args); err == nil {
t.Errorf("%v was taken", args)
}
}
got, err := run(map[string]any{"what": "applied", "node": " g14 "})
if err != nil {
t.Fatal(err)
}
if r := got.(Read); r.Count != 1 || r.Said != "1 in applied for node g14" {
t.Errorf("a read: %+v", r)
}
}
// The manifest declares the seat, its verb, the four buckets this code keeps, and an account of its own, so that
// its account alone (with each client for its machine's keys) writes them.
func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
RunsAs string `json:"runs-as"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
State []string `json:"state"`
OwnSecrets map[string]string `json:"own-secrets"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Module != "mesh-provisioner" || m.RunsAs != "mesh-provisioner" || m.OwnSecrets["broker"] == "" {
t.Errorf("an account of its own: module %q, runs-as %q, own secrets %v", m.Module, m.RunsAs, m.OwnSecrets)
}
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || m.Claims[0].Scope != "mesh" ||
!slices.Equal(m.Claims[0].Serves, []string{Verb}) {
t.Errorf("the claim: %+v", m.Claims)
}
if want := []string{Registry, Needs, Applied, Collect}; !slices.Equal(m.State, want) {
t.Errorf("the state is %v, want %v", m.State, want)
}
}
@@ -0,0 +1,216 @@
package main
import (
"encoding/json"
"fmt"
"slices"
"sort"
"strconv"
"strings"
)
// The verb `provisioning`: a read of what this module keeps, narrowed by node and module. It writes nothing:
// the registry is this module's, and the needs, the applied and the items are each machine's client's, for its
// own machine (ADR 0349 clause 3).
// Entry is one key's current value.
type Entry struct {
Key string
Value json.RawMessage
Revision uint64
}
// Store is the bus as this module reaches it: the state it keeps.
type Store interface {
Get(state, key string) (*Entry, error)
Put(state, key string, value any) error
Delete(state, key string) error
Keys(state string) ([]string, error)
}
// Query is one call of the verb.
type Query struct {
What string
Node string
Module string
Collection string
}
// Row is one key of a bucket, its parts named, and its value as written.
type Row struct {
Key string `json:"key"`
Node string `json:"node,omitempty"`
Module string `json:"module,omitempty"`
Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"`
Collection string `json:"collection,omitempty"`
Value json.RawMessage `json:"value"`
}
// Read is a read's answer: the bucket read, its rows, how many, and what narrowed them.
type Read struct {
What string `json:"what"`
Rows []Row `json:"rows"`
Count int `json:"count"`
Said string `json:"said"`
}
// shapes are the keys of each bucket, the node first, as the clients write them.
var shapes = map[string]string{
Needs: "<node>.<module>.<kind>.<name>",
Applied: "<node>.<module>",
Collect: "<node>.<collection>.<contributor>",
}
// rowOf names a key's parts by its bucket's shape, or says the key is not in that shape.
func rowOf(what, key string) (Row, bool) {
r := Row{Key: key}
switch what {
case Registry:
r.Kind = key
return r, key != ""
case Needs:
parts := strings.SplitN(key, ".", 4)
if len(parts) != 4 || slices.Contains(parts, "") {
return r, false
}
r.Node, r.Module, r.Kind, r.Name = parts[0], parts[1], parts[2], parts[3]
case Applied:
parts := strings.Split(key, ".")
if len(parts) != 2 || slices.Contains(parts, "") {
return r, false
}
r.Node, r.Module = parts[0], parts[1]
case Collect:
parts := strings.SplitN(key, ".", 3)
if len(parts) != 3 || slices.Contains(parts, "") {
return r, false
}
// The contributor is the module whose item it is; the collection as its key's token writes it, a character a
// token cannot hold written `=<hex>` (`shell=3Azsh=3Anormal` is `shell:zsh:normal`).
collection, ok := fromKeyToken(parts[1])
if !ok {
return r, false
}
r.Node, r.Collection, r.Module = parts[0], collection, parts[2]
default:
return r, false
}
return r, true
}
// Do answers a read.
func Do(s Store, q Query) (Read, error) {
what := q.What
if what == "" {
what = Registry
}
if _, ok := shapes[what]; !ok && what != Registry {
return Read{}, fmt.Errorf("provisioning reads registry, needs, applied or collect, not %q", what)
}
if what == Registry && (q.Node != "" || q.Module != "" || q.Collection != "") {
return Read{}, fmt.Errorf("the registry is the same for every node and module; it is not narrowed")
}
if q.Collection != "" && what != Collect {
return Read{}, fmt.Errorf("collection narrows collect alone, not %s", what)
}
keys, err := s.Keys(what)
if err != nil {
return Read{}, fmt.Errorf("listing %s: %w", what, err)
}
out := Read{What: what, Rows: []Row{}}
var unshaped []string
for _, k := range keys {
r, ok := rowOf(what, k)
if !ok {
unshaped = append(unshaped, k)
continue
}
if (q.Node != "" && r.Node != q.Node) || (q.Module != "" && r.Module != q.Module) ||
(q.Collection != "" && r.Collection != q.Collection) {
continue
}
e, err := s.Get(what, k)
if err != nil {
return Read{}, fmt.Errorf("reading %s's %s: %w", what, k, err)
}
if e == nil {
continue // deleted between the listing and the read
}
r.Value = e.Value
out.Rows = append(out.Rows, r)
}
sort.Slice(out.Rows, func(i, j int) bool { return out.Rows[i].Key < out.Rows[j].Key })
out.Count = len(out.Rows)
out.Said = said(what, q, out.Count)
if len(unshaped) > 0 {
// Said, never dropped quietly: a key not in its bucket's shape is a client writing what it should not.
out.Said += fmt.Sprintf("; %d key(s) are not in the shape %s and are not listed: %s", len(unshaped),
shapes[what], strings.Join(unshaped, ", "))
}
return out, nil
}
func said(what string, q Query, n int) string {
var narrowed []string
for _, p := range [][2]string{{"node", q.Node}, {"module", q.Module}, {"collection", q.Collection}} {
if p[1] != "" {
narrowed = append(narrowed, p[0]+" "+p[1])
}
}
s := fmt.Sprintf("%d in %s", n, what)
if len(narrowed) > 0 {
s += " for " + strings.Join(narrowed, ", ")
}
return s
}
// queryOf reads a call's arguments strictly: an argument it does not know is refused, not ignored.
func queryOf(args map[string]any) (Query, error) {
var q Query
fields := map[string]*string{"what": &q.What, "node": &q.Node, "module": &q.Module, "collection": &q.Collection}
var unknown []string
for k, v := range args {
into, ok := fields[k]
if !ok {
unknown = append(unknown, k)
continue
}
if v == nil {
continue
}
s, ok := v.(string)
if !ok {
return Query{}, fmt.Errorf("%s is a string", k)
}
*into = strings.TrimSpace(s)
}
if len(unknown) > 0 {
sort.Strings(unknown)
return Query{}, fmt.Errorf("provisioning takes no %s; it takes what, node, module and collection",
strings.Join(unknown, ", "))
}
return q, nil
}
// fromKeyToken reads a key's token as the client writes it (mesh-provisioner-client collect.go): `=<hex>` is the
// character of that code.
func fromKeyToken(s string) (string, bool) {
var b strings.Builder
for i := 0; i < len(s); i++ {
if s[i] != '=' {
b.WriteByte(s[i])
continue
}
if i+2 >= len(s) {
return "", false
}
v, err := strconv.ParseUint(s[i+1:i+3], 16, 8)
if err != nil {
return "", false
}
b.WriteByte(byte(v))
i += 2
}
return b.String(), true
}
@@ -0,0 +1,156 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"slices"
)
// The registry: which domain answers which kind of placeholder (novox/hq ADR 0349 clauses 1 and 3). A kind is the
// namespace of a placeholder, `${<kind>:…}`; its answer says which module owns the values and where a node's
// provisioner client finds them for its node. Compiled here and written to the bucket mesh-provisioner_registry,
// which this module alone writes and every client reads: a domain that comes to answer a kind is a change to this
// table, through the design that moves it (design 57).
// The buckets this module keeps (its manifest's `state`), by local name.
const (
Registry = "registry"
Needs = "needs"
Applied = "applied"
Collect = "collect"
)
// Where a client finds a kind's values.
const (
// FromBucket is a domain's bucket, watched for the node's keys: a watch delivers the current values first.
FromBucket = "bucket"
// FromAsk is a seat's verb asked on request and reply: a value never kept in a bucket (ADR 0201 §13).
FromAsk = "ask"
)
// Answer is one kind's entry in the registry.
type Answer struct {
// Kind is the placeholder's namespace.
Kind string `json:"kind"`
// Domain is the module that owns the kind's values; for bound values, the module providing the provision
// the placeholder names, which is not one module but whichever the mesh resolved.
Domain string `json:"domain"`
// From is where a client finds the values: FromBucket or FromAsk.
From string `json:"from"`
// Bucket and Key, for a value in a bucket: the bucket, and the shape of a key in it, the node first.
Bucket string `json:"bucket,omitempty"`
Key string `json:"key,omitempty"`
// Ask, for a value asked for: the seat's verb, `<seat>.<verb>`.
Ask string `json:"ask,omitempty"`
// Why says the answer in a sentence, and Decision the record that decided it.
Why string `json:"why"`
Decision string `json:"decision"`
}
// placement is the controller's: where a module runs, its directories, accesses, ports, seats and the machine's
// facts (ADR 0349 clause 6). On the bus, in the provisioner's plan of each module the controller handed to it, which
// its assignment carries (mesh-controller internal/inventory Provisioning, issue 671), or for a module only pulled in
// by another, its value of its own under the same key (issue 765): a client never reads the node's declaration,
// which the node-engine keeps for root alone.
func placement(kind, what string) Answer {
return Answer{Kind: kind, Domain: "mesh-controller", From: FromBucket, Bucket: "mesh-controller_assignments",
Key: "<node>.<module>",
Why: what + ": placement, the controller's, in the provisioner's plan its assignment carries (provisioning." +
"placement)", Decision: "novox/hq ADR 0349 clause 6"}
}
// oldCollection is a namespace read as a collection of its own name (novox/hq ADR 0349 clause 13, design 57 L1).
func oldCollection(kind, what string) Answer {
return Answer{Kind: kind, Domain: "mesh-provisioner", From: FromBucket, Bucket: "mesh-provisioner_collect",
Key: "<node>.<collection>.<contributor>",
Why: what + ": read as the collection `" + kind + ":<name>`, its items recorded by the client of the node " +
"its contributors are placed on", Decision: "novox/hq ADR 0349 clause 13"}
}
// TheRegistry is every kind a moved module's template may name, and its answer, in the order of ADR 0349 clause 2.
var TheRegistry = []Answer{
{Kind: "config", Domain: "mesh-config", From: FromBucket, Bucket: "mesh-config_resolved", Key: "<node>.<module>.<key>",
Why: "a config value, resolved for each module on each node by mesh-config", Decision: "novox/hq ADR 0348"},
{Kind: "secret", Domain: "mesh-vault", From: FromAsk, Ask: "mesh-vault.get",
Why: "a shared secret, handed by the vault sealed to the client's key, on request and reply only",
Decision: "novox/hq ADR 0349 clause 11"},
// The provider's bucket and its keys, as the providers' helper writes them (mesh-sdk go/provider, issue 678): the
// bucket of the module the consumer's plan names as its provider, and the consumer's machine first.
{Kind: "bound", Domain: "provider", From: FromBucket, Bucket: "<provider-module>_bound",
Key: "<node>.<module>.<provision>[.<local>]",
Why: "a consumer's values, written by the module providing the provision to its own bucket, one entry per " +
"consumer, the bucket of the provider module the consumer's plan names", Decision: "novox/hq ADR 0349 clause 12"},
// A provider's consumers' credentials, each consumer's pair credential, which the provider machine's client asks
// the vault for and fills into a file the provider alone reads (issue 679).
{Kind: "consumers", Domain: "mesh-vault", From: FromAsk, Ask: "mesh-vault.get",
Why: "each bound consumer's credential, asked of the vault by the provider machine's client and filled into a " +
"file only the provider reads, one entry per consumer", Decision: "novox/hq ADR 0373"},
{Kind: "collected", Domain: "mesh-provisioner", From: FromBucket, Bucket: "mesh-provisioner_collect",
Key: "<node>.<collection>.<contributor>",
Why: "the items of a collection, recorded by the client of the node its contributors are placed on",
Decision: "novox/hq ADR 0349 clause 13"},
// The three namespaces the controller fills for a module not handed to the provisioner, read in a handed
// collector's template as the collection of the same name (design 57 L1): `${shell:zsh:normal}` is the
// collection `shell:zsh:normal`.
oldCollection("shell", "a shell's code in one slot, or a session file's or a power moment's"),
oldCollection("environment", "the account's environment in one format"),
oldCollection("contribution", "what a seat's holder receives of one kind"),
placement("dir", "a module's directory"),
placement("access", "a directory a module reaches of another's"),
placement("port", "a module's port"),
placement("seat", "where a seat's holder is reached"),
placement("machine", "a fact of the machine"),
placement("backup", "what the machine's modules back up: `lines` and `data`, as the controller derives them from each module's declared data"),
}
// Publish brings the registry bucket to TheRegistry: a kind whose answer differs is written, a kind no longer in
// the table is deleted, and a pass that finds them equal writes nothing. It answers the kinds it wrote and
// deleted.
func Publish(s Store) (written, deleted []string, err error) {
known := map[string]bool{}
for _, a := range TheRegistry {
known[a.Kind] = true
want, err := json.Marshal(a)
if err != nil {
return written, deleted, err
}
have, err := s.Get(Registry, a.Kind)
if err != nil {
return written, deleted, fmt.Errorf("reading the registry's %s: %w", a.Kind, err)
}
if have != nil && sameJSON(have.Value, want) {
continue
}
if err := s.Put(Registry, a.Kind, a); err != nil {
return written, deleted, fmt.Errorf("writing the registry's %s: %w", a.Kind, err)
}
written = append(written, a.Kind)
}
keys, err := s.Keys(Registry)
if err != nil {
return written, deleted, fmt.Errorf("listing the registry: %w", err)
}
for _, k := range keys {
if known[k] {
continue
}
if err := s.Delete(Registry, k); err != nil {
return written, deleted, fmt.Errorf("deleting the registry's %s, a kind no domain answers: %w", k, err)
}
deleted = append(deleted, k)
}
slices.Sort(deleted)
return written, deleted, nil
}
// sameJSON says two JSON texts hold the same value, whatever their spacing or the order of their keys.
func sameJSON(a, b []byte) bool {
var x, y any
if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil {
return bytes.Equal(a, b)
}
ja, _ := json.Marshal(x)
jb, _ := json.Marshal(y)
return bytes.Equal(ja, jb)
}
+5
View File
@@ -0,0 +1,5 @@
module mesh-provisioner
go 1.25.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 h1:I9rm8HhYRJ0+mzQQz+VAH0kgv2YJWkvctEc67Yd0PX8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
+55
View File
@@ -0,0 +1,55 @@
{
"module": "mesh-provisioner",
"version": "1",
"runs-as": "mesh-provisioner",
"claims": [
{
"name": "mesh-provisioner",
"scope": "mesh",
"serves": [
"provisioning"
]
}
],
"state": [
"registry",
"needs",
"applied",
"collect"
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-provisioner",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-provisioner",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-provisioner"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-provisioner",
"place": "."
}
],
"build": {
"artifacts": [
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-provisioner",
"binary": "mesh-provisioner",
"loads": [
"mesh-provisioner"
]
}
]
}
}