Files
mesh-catalog/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect.go
T

257 lines
8.5 KiB
Go

package main
import (
"cmp"
"fmt"
"slices"
"strconv"
"strings"
)
// Collections (novox/hq ADR 0349 clause 13, design 57 L1, issue 684): a provision with many providers. A collector,
// a module taking additions from others, names a collection in its template; each contributor ships its item as a
// template of its own. The client of a node fills each contributor's item placed there, from the same owners as a
// file, and records it in mesh-provisioner_collect at `<node>.<collection>.<contributor>`; it fills a collector's
// template from every item of the collection recorded for its node, in the contributors' order, each introduced by
// a line naming its contributor. An item goes when its contributor leaves the node, and the collector is filled
// again without it.
//
// **The three old namespaces read as collections.** `${shell:<shell>:<slot>}`, `${environment:<format>}` and
// `${contribution:<seat>:<kind>}`, which the controller fills for a module not handed to the provisioner, name in a
// handed collector's template the collection of the same name: `${shell:zsh:normal}` is `shell:zsh:normal`, the
// collection a contributor's item for zsh's normal slot is recorded in.
// CollectBucket keeps the items of every collection, `<node>.<collection>.<contributor>`.
const CollectBucket = "mesh-provisioner_collect"
// CollectedKind is the namespace of a collection's placeholder, `${collected:<collection>}`.
const CollectedKind = "collected"
// oldCollectionKinds are the namespaces read as collections of their own name.
var oldCollectionKinds = []string{"shell", "environment", "contribution"}
// collectionOf is the collection a need names, without its selector, and whether it names one.
func collectionOf(n Need) (string, bool) {
name := ""
if _, is := environmentRenderer(n); is {
return EnvironmentCollection, true
}
switch {
case n.Kind == CollectedKind:
name = n.Name
case slices.Contains(oldCollectionKinds, n.Kind):
name = n.Kind + ":" + n.Name
default:
return "", false
}
collection, _ := splitSelector(name)
return collection, true
}
// selectorOfNeed is the attributes a need's selector asks for (`${collected:c:bar=top,place=status}`), none for a
// need without one, or why the selector is none (design 57 L3, issue 777).
func selectorOfNeed(n Need) (map[string]string, error) {
name := n.Name
if n.Kind != CollectedKind {
name = n.Kind + ":" + n.Name
}
_, selector := splitSelector(name)
out := map[string]string{}
if selector == "" {
return out, nil
}
for _, pair := range strings.Split(selector, ",") {
k, v, ok := strings.Cut(pair, "=")
switch {
case !ok || !attributeKeyShape.MatchString(k) || !attributeValueShape.MatchString(v):
return nil, fmt.Errorf("%q in %q is not <attribute>=<value>", pair, selector)
case out[k] != "":
return nil, fmt.Errorf("%q names %s twice", selector, k)
}
out[k] = v
}
return out, nil
}
// splitSelector is a placeholder's name without its selector, and the selector: `blocks:bar=top,place=left` is
// `blocks` and `bar=top,place=left`. A collection's name holds no `=`, so the selector begins at the last colon
// before the first one (mesh-controller internal/catalogue collections.go).
func splitSelector(name string) (collection, selector string) {
eq := strings.Index(name, "=")
if eq < 0 {
return name, ""
}
colon := strings.LastIndex(name[:eq], ":")
if colon < 0 {
return name, ""
}
return name[:colon], name[colon+1:]
}
// ItemRecord is one contributor's item, as mesh-provisioner_collect keeps it: filled, never holding a secret.
type ItemRecord struct {
Node string `json:"node"`
Collection string `json:"collection"`
Contributor string `json:"contributor"`
Text string `json:"text"`
// Name tells the item from the contributor's others of the collection; Attributes are what a placeholder
// selecting by them is matched against, `order` among them (design 57 L3, issue 777).
Name string `json:"name,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
// Templates is the digest of the contributor's templates it was filled from.
Templates string `json:"templates,omitempty"`
}
// itemID is an item's name among its contributor's: the module, and `/<name>` for a named item. It is the
// contributor part of the item's key.
func itemID(module, name string) string {
if name == "" {
return module
}
return module + "/" + name
}
// splitID is an item's module and name.
func splitID(id string) (module, name string) {
module, name, _ = strings.Cut(id, "/")
return module, name
}
// itemValue is an item as the client holds it: its filled text and its attributes.
type itemValue struct {
text string
attributes map[string]string
}
// order is the item's place among the items of one placeholder: its `order` attribute (0 to 99), else 50.
func (v itemValue) order() int {
if n, err := strconv.Atoi(v.attributes[OrderAttribute]); err == nil && n >= 0 && n <= 99 {
return n
}
return OrderDefault
}
// OrderAttribute orders the items of a placeholder, lowest first; OrderDefault is the order of an item with none.
const (
OrderAttribute = "order"
OrderDefault = 50
)
// CollectKey is an item's key, `<node>.<collection>.<contributor>`: the collection with every character a key's
// token cannot hold written `=<hex>`, so `shell:zsh:normal` is `shell=3Azsh=3Anormal`.
func CollectKey(node, collection, contributor string) string {
return node + "." + keyToken(collection) + "." + contributor
}
func keyToken(s string) string {
var b strings.Builder
for _, r := range []byte(s) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
b.WriteByte(r)
default:
fmt.Fprintf(&b, "=%02X", r)
}
}
return b.String()
}
// fromKeyToken reads a token keyToken wrote.
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
}
// itemOfKey is an item key's collection and contributor, for this node; false for a key not in the shape.
func itemOfKey(node, key string) (collection, contributor string, ok bool) {
rest, mine := strings.CutPrefix(key, node+".")
if !mine {
return "", "", false
}
token, contributor, two := strings.Cut(rest, ".")
if !two || token == "" || contributor == "" || strings.Contains(contributor, ".") {
return "", "", false
}
if module, name := splitID(contributor); module == "" || strings.Contains(name, "/") ||
(name == "" && strings.Contains(contributor, "/")) {
return "", "", false
}
collection, ok = fromKeyToken(token)
return collection, contributor, ok && collectionShape.MatchString(collection)
}
// collectionText is a collection's value on this node: every item of it the selector takes (all without one), by
// order, then contributor, then item name, each introduced by a line naming its contributor in the collector's
// comment (none without one), each ending its line. Empty when nothing is contributed: a collection with no item is
// filled, not waited for.
func collectionText(items map[string]itemValue, comment string, where map[string]string) string {
var ids []string
for id, it := range items {
if matches(it, where) {
ids = append(ids, id)
}
}
slices.SortFunc(ids, func(a, b string) int {
if c := cmp.Compare(items[a].order(), items[b].order()); c != 0 {
return c
}
ma, na := splitID(a)
mb, nb := splitID(b)
return cmp.Or(cmp.Compare(ma, mb), cmp.Compare(na, nb))
})
var b strings.Builder
for _, id := range ids {
if comment != "" {
module, _ := splitID(id)
fmt.Fprintf(&b, "%s %s\n", comment, module)
}
text := items[id].text
b.WriteString(text)
if !strings.HasSuffix(text, "\n") {
b.WriteString("\n")
}
}
return b.String()
}
// matches is whether the item carries every attribute the selector asks for.
func matches(it itemValue, where map[string]string) bool {
for k, v := range where {
if got, has := it.attributes[k]; !has || got != v {
return false
}
}
return true
}
// collectionNamed is the plan's collection of that name.
func (p Plan) collectionNamed(name string) (PlannedCollection, bool) {
for _, c := range p.Collections {
if c.Name == name {
return c, true
}
}
return PlannedCollection{}, false
}
// collects is whether the plan collects that collection.
func (p Plan) collects(name string) bool {
_, ok := p.collectionNamed(name)
return ok
}