The controller plans config, templates and moved providers' provisions, hands bus accounts to the vault and judges the new manifest words, on the feature's SDK and node-engine validator (feature 57, config and provisioning).
636 lines
28 KiB
Go
636 lines
28 KiB
Go
package catalogue
|
||
|
||
import (
|
||
"fmt"
|
||
"path"
|
||
"regexp"
|
||
"slices"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// Collections (novox/hq ADR 0349 clause 13, design 57 L1, issue 684): a provision with many providers.
|
||
//
|
||
// **A collector requires a collection; a contributor provides an item from a template.** A module taking additions
|
||
// from others names a collection in a file of its own, `${collected:<collection>}`, and declares it under
|
||
// `collections`, with the comment its tool writes a line in. Each contributor ships its item as a template of its
|
||
// own repository and declares it under `items`: `{"collection": …, "template": "files/<name>"}`. Its build packs the
|
||
// template with the module's others, and the mesh holds its text as it holds a file's (template.go).
|
||
//
|
||
// **The node's provisioner client fills both.** An item is filled on the node by the client of the node its
|
||
// contributor is placed on, from the same owners as a file, and recorded in mesh-provisioner_collect; a collector's
|
||
// file is filled by the same client from every item of its collection recorded for that node, so an item goes when
|
||
// its contributor leaves. So a module contributing an item is a moved module (handed.go), and the controller renders
|
||
// no item: the plan of a contributor names its items and their templates, and the plan of a collector names each
|
||
// collection it reads and its comment. Neither carries a contributor's text.
|
||
//
|
||
// **The three old namespaces read as collections.** `${shell:<shell>:<slot>}`, `${environment:<format>}` and
|
||
// `${contribution:<seat>:<kind>}` name, in a handed collector's file, the collection of the same name —
|
||
// `shell:zsh:normal` — with the comment the controller wrote before each module's piece. An item is contributed to
|
||
// one by that name, by a module the old form would have let contribute it. Each file is filled one way, never
|
||
// both (ADR 0349 clause 17): a handed collector is filled from items alone and an unhanded one from the old forms
|
||
// alone, and a piece of the other form on the same machine is said as not placed, never dropped quietly.
|
||
|
||
// Item is one item a module contributes to a collection: its template in the module's repository, or, in the
|
||
// manifest the mesh holds, the template's text, read back from the build's templates archive like a file's.
|
||
//
|
||
// **Attributes, a capability and a name** (design 57 L3, issue 777): an item may carry `attributes` (string keys and
|
||
// values; `order`, a whole number 0–99 defaulting to 50, orders the items of one placeholder) and `if-capability`,
|
||
// the capability a machine must report for the item to be placed on it. A module may give several items to one
|
||
// collection, each then with a `name` unique within the module and the collection. A collector takes only the items
|
||
// whose attributes match, `${collected:<collection>:<k>=<v>,…}`, and all of them without the selector.
|
||
type Item struct {
|
||
Collection string `json:"collection"`
|
||
Name string `json:"name,omitempty"`
|
||
Template string `json:"template,omitempty"`
|
||
Content string `json:"content,omitempty"`
|
||
Attributes map[string]string `json:"attributes,omitempty"`
|
||
IfCapability string `json:"if-capability,omitempty"`
|
||
}
|
||
|
||
// ItemKey is an item's key among its module's: the collection, and `/<name>` for a named item.
|
||
func ItemKey(collection, name string) string {
|
||
if name == "" {
|
||
return collection
|
||
}
|
||
return collection + "/" + name
|
||
}
|
||
|
||
func (it Item) key() string { return ItemKey(it.Collection, it.Name) }
|
||
|
||
// order is the item's place among the items of one placeholder: its `order` attribute, or OrderDefault.
|
||
func (it Item) order() int {
|
||
if n, err := strconv.Atoi(it.Attributes[OrderAttribute]); err == nil && n >= 0 && n <= 99 {
|
||
return n
|
||
}
|
||
return OrderDefault
|
||
}
|
||
|
||
// OrderAttribute is the attribute that orders items.
|
||
const OrderAttribute = "order"
|
||
|
||
// matches is whether the item's attributes carry every pair of the selector.
|
||
func (it Item) matches(where map[string]string) bool {
|
||
for k, v := range where {
|
||
if got, has := it.Attributes[k]; !has || got != v {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// itemsInOrder is the module's items by order, then name; the order the old form placed a module's pieces in.
|
||
func itemsInOrder(items []Item) []Item {
|
||
out := append([]Item(nil), items...)
|
||
sort.SliceStable(out, func(a, b int) bool {
|
||
if oa, ob := out[a].order(), out[b].order(); oa != ob {
|
||
return oa < ob
|
||
}
|
||
return out[a].Name < out[b].Name
|
||
})
|
||
return out
|
||
}
|
||
|
||
// Collection is one collection a module collects, and the comment its tool writes a line in: each contributor's item
|
||
// is introduced by `<comment> <contributor>`, and by nothing without one.
|
||
type Collection struct {
|
||
Name string `json:"name"`
|
||
Comment string `json:"comment,omitempty"`
|
||
}
|
||
|
||
// CollectedNamespace is a collection's placeholder namespace, `${collected:<collection>}`.
|
||
const CollectedNamespace = "collected"
|
||
|
||
// EnvironmentCollection is the one collection of the session environment (design 57 L3, issue 795): its items are
|
||
// typed by attributes, a variable (`kind=variable`, `name`) or a PATH entry (`kind=path`, `at=start|end`), and the
|
||
// template's text is the value or the entry. A collector names the rendering, `${collected:environment:posix}` or
|
||
// `${collected:environment:systemd}`, which is not a selector.
|
||
const EnvironmentCollection = "environment"
|
||
|
||
// oldCollectionNamespaces are the namespaces read as collections of their own name.
|
||
var oldCollectionNamespaces = []string{"shell", "environment", "contribution"}
|
||
|
||
var (
|
||
// ofCollected is where a collector places a collection. Loose inside the braces, so a misspelt name is refused.
|
||
ofCollected = regexp.MustCompile(`\$\{collected:([^}]*)\}`)
|
||
// collectionName is words joined by colons, so an old namespace's placeholder names its collection as it reads.
|
||
collectionName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*(:[a-z0-9][a-z0-9-]*)*$`)
|
||
// itemName, attributeKey and attributeValue are the words an item's name and attributes are made of; none holds
|
||
// a colon, a comma or an equals sign, which tell a selector from a collection's name.
|
||
itemName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
||
attributeKey = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
|
||
attributeValue = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]*$`)
|
||
)
|
||
|
||
// splitSelector is a placeholder's name without its selector, and the selector: `widgets:bar=top,place=left` is
|
||
// `widgets` and `bar=top,place=left`. A collection's name holds no `=`, so the selector begins at the last colon
|
||
// before the first one.
|
||
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:]
|
||
}
|
||
|
||
// selectorOf is a selector's pairs, or why it is none.
|
||
func selectorOf(selector string) (map[string]string, error) {
|
||
out := map[string]string{}
|
||
if selector == "" {
|
||
return out, nil
|
||
}
|
||
for _, pair := range strings.Split(selector, ",") {
|
||
k, v, ok := strings.Cut(pair, "=")
|
||
switch {
|
||
case !ok || !attributeKey.MatchString(k) || !attributeValue.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
|
||
}
|
||
|
||
// collectionOfPlaceholder is the collection a placeholder's namespace and key name, and whether they name one.
|
||
func collectionOfPlaceholder(namespace, key string) (string, bool) {
|
||
key, _ = splitSelector(key)
|
||
switch {
|
||
case namespace == CollectedNamespace && strings.HasPrefix(key, EnvironmentCollection+":"):
|
||
// The environment is one collection, and the rest of the name picks the rendering (issue 795).
|
||
return EnvironmentCollection, true
|
||
case namespace == CollectedNamespace:
|
||
return key, true
|
||
case namespace == "environment":
|
||
return EnvironmentCollection, true
|
||
case slices.Contains(oldCollectionNamespaces, namespace):
|
||
return namespace + ":" + key, true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// oldCollection is an old namespace's collection by its name: its comment, or why no item may be contributed to it.
|
||
// ok is false for a name that is not an old namespace's.
|
||
//
|
||
// forItem is whether an item is to be contributed to it: an item may be contributed to a kind received as data,
|
||
// which a collector may place as a collection only where its shape says its pieces are text (design 57 L3).
|
||
func oldCollection(name string, forItem bool) (comment string, old bool, wrong error) {
|
||
namespace, key, _ := strings.Cut(name, ":")
|
||
if !slices.Contains(oldCollectionNamespaces, namespace) {
|
||
return "", false, nil
|
||
}
|
||
switch namespace {
|
||
case "shell":
|
||
target, slot, two := strings.Cut(key, ":")
|
||
if !two || !oneOf(contributionTargets(), target) || !oneOf(knownSlots, slot) {
|
||
return "", true, fmt.Errorf("%s is no slot: shell code is shell:<shell>:<slot>, the shell one of %s, "+
|
||
"the session's file one of %s or the power moment one of %s, and the slot one of %s", name,
|
||
strings.Join(knownShells, ", "), strings.Join(sessionFiles, ", "), strings.Join(powerMoments, ", "),
|
||
strings.Join(knownSlots, ", "))
|
||
}
|
||
// shellCode's line before each module's code.
|
||
return "#", true, nil
|
||
case "environment":
|
||
if key == "" {
|
||
// The collection itself, which an item is contributed to; a placeholder names a rendering.
|
||
return "#", true, nil
|
||
}
|
||
if key != EnvironmentPOSIX && key != EnvironmentSystemd {
|
||
return "", true, fmt.Errorf("%s is no format: the environment is environment:%s or environment:%s",
|
||
name, EnvironmentPOSIX, EnvironmentSystemd)
|
||
}
|
||
return "#", true, nil
|
||
}
|
||
seat, kind, _ := strings.Cut(key, ":")
|
||
s, r, ok := receivable(seat, kind)
|
||
switch {
|
||
case !ok && s.Name == "":
|
||
return "", true, fmt.Errorf("%s names the seat %q, which the mesh does not define", name, seat)
|
||
case !ok:
|
||
return "", true, fmt.Errorf("%s: %s; %s", name, s.Name, kindsOf(s))
|
||
case r.Shape != nil && (forItem || collectableAsText(r)):
|
||
return r.Comment, true, nil
|
||
case r.Shape != nil:
|
||
return "", true, fmt.Errorf("%s is received by %s as data in its own shape, which no item of text is "+
|
||
"(novox/hq ADR 0255)", name, s.Name)
|
||
case r.Dirs:
|
||
return "", true, fmt.Errorf("%s names its contributors' directories, which the holder reads as paths "+
|
||
"the controller fills; it is not yet a collection (design 57 L1)", name)
|
||
case s.Name != canonicalSeat(seat):
|
||
return "", true, fmt.Errorf("%s names the seat by another name; a collection names it %s", name, s.Name)
|
||
}
|
||
return r.Comment, true, nil
|
||
}
|
||
|
||
// placerOfOld is the seat whose holder alone may collect an old namespace's collection (ADR 0203, 0204, 0208,
|
||
// 0211, 0212): the same seat that places it the old way.
|
||
func placerOfOld(name string) string {
|
||
namespace, key, _ := strings.Cut(name, ":")
|
||
switch namespace {
|
||
case "shell":
|
||
target, _, _ := strings.Cut(key, ":")
|
||
return placerOf(target)
|
||
case "environment":
|
||
return EnvironmentSeat
|
||
}
|
||
seat, _, _ := strings.Cut(key, ":")
|
||
return canonicalSeat(seat)
|
||
}
|
||
|
||
// collectionProblems is what is wrong with a module's collections and items, from the manifest alone.
|
||
func collectionProblems(m Manifest) []string {
|
||
var problems []string
|
||
declared := map[string]bool{}
|
||
for i, c := range m.Collections {
|
||
switch {
|
||
case !collectionName.MatchString(c.Name):
|
||
problems = append(problems, fmt.Sprintf("%s's collection %d is %q, which is not a collection's name: "+
|
||
"words of lower-case letters, digits and hyphens, joined by colons", m.Module, i+1, c.Name))
|
||
case declared[c.Name]:
|
||
problems = append(problems, fmt.Sprintf("%s collects %s twice", m.Module, c.Name))
|
||
}
|
||
if _, old, _ := oldCollection(c.Name, false); old {
|
||
problems = append(problems, fmt.Sprintf("%s collects %s, which is read from its placeholder: a module "+
|
||
"places it as ${%s} where it holds the seat that places it, and declares nothing", m.Module, c.Name,
|
||
c.Name))
|
||
}
|
||
if strings.ContainsAny(c.Comment, "\n\r") {
|
||
problems = append(problems, fmt.Sprintf("%s's collection %s has a comment holding a line break",
|
||
m.Module, c.Name))
|
||
}
|
||
declared[c.Name] = true
|
||
}
|
||
// A collection declared and placed nowhere is a word read by nothing; judged where every file's text is here.
|
||
if !m.Templated() {
|
||
named := map[string]bool{}
|
||
for _, r := range m.Resources {
|
||
if text, ok := r["content"].(string); ok {
|
||
for _, f := range ofCollected.FindAllStringSubmatch(text, -1) {
|
||
base, _ := splitSelector(f[1])
|
||
named[base] = true
|
||
}
|
||
}
|
||
}
|
||
for _, c := range m.Collections {
|
||
if !named[c.Name] && collectionName.MatchString(c.Name) {
|
||
problems = append(problems, fmt.Sprintf("%s collects %s and no file of its places it as "+
|
||
"${collected:%s}", m.Module, c.Name, c.Name))
|
||
}
|
||
}
|
||
}
|
||
contributed := map[string]bool{}
|
||
for i, it := range m.Items {
|
||
where := fmt.Sprintf("%s's item %d", m.Module, i+1)
|
||
if !collectionName.MatchString(it.Collection) {
|
||
problems = append(problems, fmt.Sprintf("%s is to %q, which is not a collection's name", where,
|
||
it.Collection))
|
||
} else if _, old, wrong := oldCollection(it.Collection, true); old && wrong != nil {
|
||
problems = append(problems, fmt.Sprintf("%s: %v", where, wrong))
|
||
}
|
||
switch {
|
||
case it.Name != "" && !itemName.MatchString(it.Name):
|
||
problems = append(problems, fmt.Sprintf("%s: the name %q is not a name: lower-case letters, digits and "+
|
||
"hyphens", where, it.Name))
|
||
case contributed[it.key()]:
|
||
problems = append(problems, fmt.Sprintf("%s contributes to %s twice; a module contributes one item to "+
|
||
"a collection, or several each with a name of its own", m.Module, it.key()))
|
||
case it.Name == "" && slices.ContainsFunc(m.Items, func(o Item) bool {
|
||
return o.Collection == it.Collection && o.Name != ""
|
||
}):
|
||
problems = append(problems, fmt.Sprintf("%s contributes several items to %s and %s has no name; each "+
|
||
"of several items has a name of its own", m.Module, it.Collection, where))
|
||
}
|
||
contributed[it.key()] = true
|
||
problems = append(problems, itemAttributeProblems(m.Module, where, it)...)
|
||
if it.Collection == EnvironmentCollection {
|
||
problems = append(problems, environmentItemProblems(where, it)...)
|
||
}
|
||
switch {
|
||
case it.Template == "" && it.Content == "":
|
||
problems = append(problems, fmt.Sprintf("%s names no template: an item is a template of the module's "+
|
||
"own, \"template\": \"%s/<name>\" (novox/hq ADR 0349 §13)", where, m.TemplatesDir()))
|
||
case it.Template != "" && it.Content != "":
|
||
problems = append(problems, fmt.Sprintf("%s names its template and says its content too", where))
|
||
case it.Template != "":
|
||
if wrong := itemTemplateProblem(m, it.Template); wrong != "" {
|
||
problems = append(problems, fmt.Sprintf("%s: %s", where, wrong))
|
||
}
|
||
// **Filled by the provisioner's client alone**: the controller renders no item.
|
||
if !m.Moved {
|
||
problems = append(problems, fmt.Sprintf("%s: an item is filled by the provisioner's client of the "+
|
||
"node its contributor is placed on, so a module contributing one is marked moved "+
|
||
"(\"moved\": true; novox/hq ADR 0349 §13, §17)", where))
|
||
}
|
||
}
|
||
if it.Content != "" {
|
||
problems = append(problems, itemTextProblems(m.Module, it)...)
|
||
}
|
||
}
|
||
return problems
|
||
}
|
||
|
||
// itemAttributeProblems is what is wrong with an item's attributes and capability. The attributes of an item of a kind
|
||
// received as data are the fields its shape places by (and `order`), each with a value the shape takes.
|
||
func itemAttributeProblems(module, where string, it Item) []string {
|
||
var problems []string
|
||
for _, k := range sortedKeys(it.Attributes) {
|
||
v := it.Attributes[k]
|
||
switch {
|
||
case !attributeKey.MatchString(k):
|
||
problems = append(problems, fmt.Sprintf("%s: the attribute %q is not a name: lower-case letters, digits "+
|
||
"and hyphens", where, k))
|
||
continue
|
||
case !attributeFits(it.Collection, k, v):
|
||
problems = append(problems, fmt.Sprintf("%s: %s=%q is not a value: lower-case letters, digits, hyphens, "+
|
||
"dots and underscores (an environment variable's name, letters, digits and underscores)", where, k, v))
|
||
continue
|
||
}
|
||
if n, err := strconv.Atoi(v); k == OrderAttribute && (err != nil || n < 0 || n > 99) {
|
||
problems = append(problems, fmt.Sprintf("%s: order=%q is not a whole number from 0 to 99", where, v))
|
||
}
|
||
}
|
||
if namespace, key, _ := strings.Cut(it.Collection, ":"); namespace == "contribution" {
|
||
seat, kind, _ := strings.Cut(key, ":")
|
||
if _, r, ok := receivable(seat, kind); ok && r.Shape != nil {
|
||
for _, k := range sortedKeys(it.Attributes) {
|
||
var place *PlaceField
|
||
for i := range r.Shape.Places {
|
||
if r.Shape.Places[i].Field == k {
|
||
place = &r.Shape.Places[i]
|
||
}
|
||
}
|
||
switch {
|
||
case k == OrderAttribute:
|
||
case place == nil:
|
||
problems = append(problems, fmt.Sprintf("%s: %s is placed by %s, and %q is none of them", where,
|
||
it.Collection, placeFields(r), k))
|
||
case !oneOf(place.Values, it.Attributes[k]):
|
||
problems = append(problems, fmt.Sprintf("%s: %s=%s; %s is one of %s", where, k, it.Attributes[k], k,
|
||
strings.Join(place.Values, ", ")))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
switch {
|
||
case it.IfCapability == "":
|
||
case !capabilityName.MatchString(it.IfCapability):
|
||
problems = append(problems, fmt.Sprintf("%s: if-capability %q is not a capability's name", where, it.IfCapability))
|
||
case !oneOf(KnownCapabilities, it.IfCapability):
|
||
problems = append(problems, fmt.Sprintf("%s: if-capability %q is one no machine reports; the node-engine "+
|
||
"detects %s", where, it.IfCapability, strings.Join(KnownCapabilities, ", ")))
|
||
}
|
||
return problems
|
||
}
|
||
|
||
// AttributeFits is whether an item's attribute is of the shape its collection allows: words of lower case, except
|
||
// the environment's `name`, which is a variable's name (design 57 L3, issue 795). The same rule is mesh-catalog's
|
||
// provisioner client's, and the inventory's.
|
||
func attributeFits(collection, k, v string) bool {
|
||
if collection == EnvironmentCollection && k == "name" {
|
||
return variableName.MatchString(v)
|
||
}
|
||
return attributeKey.MatchString(k) && attributeValue.MatchString(v)
|
||
}
|
||
|
||
// environmentItemProblems is what is wrong with an item of the environment collection: typed by `kind`, a variable
|
||
// with its `name`, a PATH entry `at` the start or the end, and no attribute besides `order`.
|
||
func environmentItemProblems(where string, it Item) []string {
|
||
var problems []string
|
||
for _, k := range sortedKeys(it.Attributes) {
|
||
if !slices.Contains([]string{"kind", "name", "at", OrderAttribute}, k) {
|
||
problems = append(problems, fmt.Sprintf("%s: %s is typed by kind, name and at, and %q is none of them",
|
||
where, it.Collection, k))
|
||
}
|
||
}
|
||
switch kind := it.Attributes["kind"]; kind {
|
||
case "variable":
|
||
if it.Attributes["name"] == "" || it.Attributes["at"] != "" {
|
||
problems = append(problems, fmt.Sprintf("%s: a variable names itself with name=<NAME> and has no at", where))
|
||
} else if it.Attributes["name"] == "PATH" {
|
||
problems = append(problems, fmt.Sprintf("%s: PATH is not a variable; a PATH entry is kind=path", where))
|
||
}
|
||
case "path":
|
||
if it.Attributes["name"] != "" || (it.Attributes["at"] != PathAtStart && it.Attributes["at"] != PathAtEnd) {
|
||
problems = append(problems, fmt.Sprintf("%s: a PATH entry is at=%s or at=%s and has no name", where,
|
||
PathAtStart, PathAtEnd))
|
||
}
|
||
default:
|
||
problems = append(problems, fmt.Sprintf("%s: an item of %s is kind=variable or kind=path, not %q", where,
|
||
it.Collection, kind))
|
||
}
|
||
return problems
|
||
}
|
||
|
||
// itemTemplateProblem is what is wrong with an item's template path, or "".
|
||
func itemTemplateProblem(m Manifest, name string) string {
|
||
dir := m.TemplatesDir()
|
||
if strings.HasPrefix(name, "/") || path.Clean(name) != name || !strings.HasPrefix(name, dir+"/") ||
|
||
strings.Contains(name, "\\") {
|
||
return fmt.Sprintf("the template %q is not a file under %s/ of its own repository: a template is %q, a "+
|
||
"plain relative path, and the build packs only %s/", name, dir, dir+"/<name>", dir)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// itemTextProblems is what an item's text may not name: a secret or a bound value, since an item is kept in a
|
||
// bucket and no secret is (ADR 0349 clause 7), and a collection, since an item is not a collector. And every
|
||
// placeholder no pass fills, as in a file's content.
|
||
func itemTextProblems(module string, it Item) []string {
|
||
var problems []string
|
||
for _, f := range templatePlaceholder.FindAllStringSubmatch(it.Content, -1) {
|
||
namespace, key := f[1], f[2]
|
||
switch _, collection := collectionOfPlaceholder(namespace, key); {
|
||
case namespace == "secret" || namespace == "bound":
|
||
problems = append(problems, fmt.Sprintf("%s's item of %s names %s: an item is kept in a bucket, where "+
|
||
"no secret is (novox/hq ADR 0349 §7)", module, it.Collection, f[0]))
|
||
case collection:
|
||
problems = append(problems, fmt.Sprintf("%s's item of %s names %s: an item is not a collector",
|
||
module, it.Collection, f[0]))
|
||
}
|
||
}
|
||
return append(problems, unconsumedPlaceholders(module, map[string]any{"id": "item:" + it.key(),
|
||
"type": "file", "content": it.Content})...)
|
||
}
|
||
|
||
// isOld is whether a collection's name is an old namespace's.
|
||
func isOld(name string) bool {
|
||
_, old, _ := oldCollection(name, true)
|
||
return old
|
||
}
|
||
|
||
// collectedPlaceholderProblems is what is wrong with one resource's ${collected:…}: placed only in a file's content,
|
||
// and naming a collection the module declares it collects.
|
||
func collectedPlaceholderProblems(m Manifest, r map[string]any) []string {
|
||
var problems []string
|
||
for _, field := range sortedKeys(r) {
|
||
v, ok := r[field].(string)
|
||
if !ok {
|
||
continue
|
||
}
|
||
for _, f := range ofCollected.FindAllStringSubmatch(v, -1) {
|
||
base, selector := splitSelector(f[1])
|
||
if _, err := selectorOf(selector); err != nil {
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v names %s: %v", m.Module, r["id"], f[0], err))
|
||
continue
|
||
}
|
||
switch {
|
||
case field != "content":
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v names %s in its %s; a collection is placed "+
|
||
"only in a file's content", m.Module, r["id"], f[0], field))
|
||
case isOld(base):
|
||
if _, _, wrong := oldCollection(base, false); wrong != nil {
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v names %s: %v", m.Module, r["id"], f[0], wrong))
|
||
}
|
||
case !slices.ContainsFunc(m.Collections, func(c Collection) bool { return c.Name == base }):
|
||
problems = append(problems, fmt.Sprintf("%s's resource %v names %s, and %s does not collect %q: "+
|
||
"declare it under collections (novox/hq ADR 0349 §13)", m.Module, r["id"], f[0], m.Module, base))
|
||
}
|
||
}
|
||
}
|
||
return problems
|
||
}
|
||
|
||
// collectionComment is the comment of a collection a module's file names: declared, or the old namespace's.
|
||
func collectionComment(m Manifest, name string) (string, error) {
|
||
if comment, old, wrong := oldCollection(name, false); old {
|
||
return comment, wrong
|
||
}
|
||
for _, c := range m.Collections {
|
||
if c.Name == name {
|
||
return c.Comment, nil
|
||
}
|
||
}
|
||
return "", fmt.Errorf("%s names ${collected:%s} and does not collect it", m.Module, name)
|
||
}
|
||
|
||
// itemsLeftOut says each item of a collection a file is filled with the old way that the controller could not render
|
||
// there (heldItemText): the collector is not handed to the provisioner on this machine (ADR 0349 clause 17). Said,
|
||
// never dropped quietly.
|
||
func itemsLeftOut(modules []Manifest, holder Manifest, collection string, facts map[string]string) []string {
|
||
return itemsLeftOutFor(modules, holder, collection, nil, facts, nil)
|
||
}
|
||
|
||
// itemsLeftOutFor is itemsLeftOut for a placeholder that selects by attributes (where), on a machine with these
|
||
// capabilities: an item the selector does not take, or whose capability the machine lacks, is not left out but not
|
||
// wanted here.
|
||
func itemsLeftOutFor(modules []Manifest, holder Manifest, collection string, where map[string]string,
|
||
facts map[string]string, caps map[string]bool) []string {
|
||
var out []string
|
||
for _, m := range inModuleOrder(modules) {
|
||
for _, it := range itemsInOrder(m.Items) {
|
||
if it.Collection != collection || !it.matches(where) || (it.IfCapability != "" && !caps[it.IfCapability]) {
|
||
continue
|
||
}
|
||
if _, rendered := heldText(it, facts); rendered && renderedOldWay(collection) {
|
||
continue
|
||
}
|
||
out = append(out, fmt.Sprintf("%s's item of %s is not placed: %s is filled the old way on this "+
|
||
"machine, from contributions, and an item only by the provisioner's client (design 57 L1)",
|
||
m.Module, it.key(), holder.Module))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// oldFormsLeftOut says each piece of an old form a handed collector's collections would have held: the collector is
|
||
// filled from items alone, and the piece's module contributes none (ADR 0349 clause 17).
|
||
func oldFormsLeftOut(modules []Manifest, holder Manifest, collections []PlanCollection) []string {
|
||
var out []string
|
||
for _, c := range collections {
|
||
namespace, key, _ := strings.Cut(c.Name, ":")
|
||
for _, m := range inModuleOrder(modules) {
|
||
old := false
|
||
switch namespace {
|
||
case "shell":
|
||
target, slot, _ := strings.Cut(key, ":")
|
||
old = slices.ContainsFunc(m.Shell, func(s ShellCode) bool { return s.For == target && s.Slot == slot })
|
||
case "environment":
|
||
old = m.Environment != nil && (len(m.Environment.Variables) > 0 || len(m.Environment.Path) > 0)
|
||
case "contribution":
|
||
seat, kind, _ := strings.Cut(key, ":")
|
||
old = slices.ContainsFunc(m.allContributions(), func(s SeatContribution) bool {
|
||
return canonicalSeat(s.Seat) == canonicalSeat(seat) && s.Kind == kind
|
||
})
|
||
}
|
||
if old {
|
||
out = append(out, fmt.Sprintf("%s's %s is not placed: %s is filled by the provisioner's client on "+
|
||
"this machine, from items, and %s contributes it the old way (design 57 L1)", m.Module, c.Name,
|
||
holder.Module, m.Module))
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// heldItemText is a moved contributor's items of an old namespace's collection as the controller renders them into a
|
||
// collector not handed on this machine (design 57 L2, issue 681): the text the mesh holds, read back from the
|
||
// contributor's templates archive, with the machine's facts filled, each ending its line. As a moved module's file
|
||
// keeps its text for a machine that cannot take it handed (handed.go), an item keeps its place in a collector filled
|
||
// the old way, so the file stays what its old form made it. Only `shell:` and `contribution:` collections, which the
|
||
// controller renders as text; false for a module with no such item, or an item whose text the mesh does not hold or
|
||
// which names anything but a machine fact this machine has, which is then said as not placed.
|
||
func heldItemText(m Manifest, collection string, facts map[string]string) (string, bool) {
|
||
items := heldItems(m, collection, nil, facts, nil)
|
||
var b strings.Builder
|
||
for _, h := range items {
|
||
b.WriteString(h.text)
|
||
}
|
||
return b.String(), len(items) > 0
|
||
}
|
||
|
||
// renderedOldWay is whether the controller renders a collection's items into a collector not handed.
|
||
func renderedOldWay(collection string) bool {
|
||
namespace, _, _ := strings.Cut(collection, ":")
|
||
return namespace == "shell" || namespace == "contribution" || namespace == EnvironmentCollection
|
||
}
|
||
|
||
// heldItem is one item of a module rendered the old way.
|
||
type heldItem struct {
|
||
item Item
|
||
text string
|
||
}
|
||
|
||
// heldItems is a module's items of the collection that the selector takes and the machine's capabilities allow, by
|
||
// order then name, each rendered as heldText does; the ones it cannot render are left out (itemsLeftOutFor says so).
|
||
func heldItems(m Manifest, collection string, where map[string]string, facts map[string]string,
|
||
caps map[string]bool) []heldItem {
|
||
if !renderedOldWay(collection) {
|
||
return nil
|
||
}
|
||
var out []heldItem
|
||
for _, it := range itemsInOrder(m.Items) {
|
||
if it.Collection != collection || !it.matches(where) || (it.IfCapability != "" && !caps[it.IfCapability]) {
|
||
continue
|
||
}
|
||
if text, ok := heldText(it, facts); ok {
|
||
out = append(out, heldItem{item: it, text: text})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// heldText is an item's text with the machine's facts filled, ending its line; false for an item whose text the mesh
|
||
// does not hold or which names anything but a machine fact this machine has.
|
||
func heldText(it Item, facts map[string]string) (string, bool) {
|
||
if it.Content == "" {
|
||
return "", false
|
||
}
|
||
text := it.Content
|
||
for _, f := range templatePlaceholder.FindAllStringSubmatch(text, -1) {
|
||
value, has := facts[f[2]]
|
||
if f[1] != "machine" || !has {
|
||
return "", false
|
||
}
|
||
text = strings.ReplaceAll(text, f[0], value)
|
||
}
|
||
if !strings.HasSuffix(text, "\n") {
|
||
text += "\n"
|
||
}
|
||
return text, true
|
||
}
|