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

157 lines
7.6 KiB
Go

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)
}