257 lines
8.5 KiB
Go
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
|
|
}
|