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:}`, 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/"}`. 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::}`, `${environment:}` and // `${contribution::}` 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::=,…}`, 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 `/` 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 ` `, 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:}`. 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 =", 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::, 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/\" (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= 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+"/", 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 }