217 lines
6.1 KiB
Go
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
|
|
}
|