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

217 lines
6.1 KiB
Go

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
}