Files
mesh-controller/internal/catalogue/handed.go
T

800 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package catalogue
import (
"errors"
"fmt"
"maps"
"regexp"
"slices"
"sort"
"strings"
)
// A module handed to the provisioner (novox/hq ADR 0349 clauses 2, 4, 6 and 17, design 57 P6, issue 672).
//
// **Marked in its manifest, one module at a time.** A module whose manifest says `"moved": true` has moved: each
// file of it that names a template is filled by the provisioner's client on the node, from the templates archive
// its build packed, and the controller fills nothing in it. Every other resource of the module, and every file of
// a module not marked, is composed exactly as before (clause 17: each file the old way or the new way, never both).
//
// **The build keeps what the client needs.** Resolving a moved module's build keeps, beside the files' text the
// mesh holds, where its templates archive is and what it hashes to, and which template each file is filled from
// (Handed). The text stays, so a machine whose node-engine cannot take a handed file is still sent the module the
// old way, byte for byte.
//
// **Handed only where the machine can take it.** A node's declaration hands a moved module's files over only when
// its node-engine writes a filled file on the client's word alone (Rendering.Hands) and the node runs a provisioner
// client (Rendering.Provisioner): then each such file is declared with its path, mode, owner and who owns it at
// runtime and without its text, the declaration names it among the filled files, and the module's plan says the
// client where its templates are and the value of every placement placeholder they name. Anywhere else the module
// composes as if it were not marked.
// MovedField is the manifest's key marking a module moved.
const MovedField = "moved"
// Handed is what a moved module's build kept for the provisioner (novox/hq ADR 0349 §2): its templates archive, as
// recorded (store-relative, routed at composition like any archive), and its digest; and, for each file resource
// filled from a template, that template's path inside the archive. Written by Resolve, never by hand.
type Handed struct {
Source string `json:"source"`
Digest string `json:"digest"`
Templates map[string]string `json:"templates"`
// Items is, for each item the module contributes, by ItemKey (its collection, and `/<name>` for a named one),
// that item's template inside the archive (collections.go, design 57 L1).
Items map[string]string `json:"items,omitempty"`
}
// Plan is a moved module's plan on one node: what the node's provisioner client fills its files from. The shape
// of the assignments mirror's `provisioning` (mesh-controller internal/inventory Provisioning).
type Plan struct {
Templates PlanTemplates `json:"templates"`
Files []PlanFile `json:"files"`
Placement map[string]string `json:"placement,omitempty"`
// Secrets are the module's own secrets this declaration names (design 57 V4, issue 647): each its templates
// name, and each it keeps in a file of its own, with that file's path. By name, never a value: the client asks
// the vault for each, and the vault hands one only when the plan names it.
Secrets []PlanSecret `json:"secrets,omitempty"`
// Items are the module's items of collections (design 57 L1): each filled by the client like a file and recorded
// in mesh-provisioner_collect for its node. Collections are the collections its files name, as their collector,
// each with its comment. By template and name, never a contributor's text.
Items []PlanItem `json:"items,omitempty"`
Collections []PlanCollection `json:"collections,omitempty"`
// Provisions are what it requires here, each with the provider resolution chose to serve it (issue 760), so the
// vault sees a consumer bind to a provider and unbind from it (novox/hq ADR 0349 clause 8); and, for a
// credential the vault keeps for it alone, the name its files call it and the file holding it (issue 757). By
// name, never a value.
Provisions []PlanProvision `json:"provisions,omitempty"`
}
// PlanItem is one item the module contributes: to which collection, and its template inside the archive.
//
// A named item has its Name and Attributes (design 57 L3, issue 777): the client records them with the item and
// places it under a placeholder selecting by them. An item whose `if-capability` the machine lacks is not in the plan.
type PlanItem struct {
Collection string `json:"collection"`
Name string `json:"name,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
Template string `json:"template"`
}
// PlanCollection is one collection the module collects, and the comment introducing each contributor's item.
type PlanCollection struct {
Name string `json:"name"`
Comment string `json:"comment,omitempty"`
}
// PlanProvision is one provision the module requires, under one local name, and the provider serving it: the module
// (ProviderModule) on the machine (Provider) that resolution chose for this consumer. Where several providers offer
// a mesh-scoped provision, it is the one this consumer is bound to (a pin, the seat's holder, or where its data is),
// never the others (issue 760). Local is the name the consumer keeps the credential under where it keeps several
// from one requirement (ADR 0094), empty for the ordinary one; a consumer has one provider per provision and local
// name.
//
// Name and Path are set only for a credential the vault keeps for this holder alone, which its machine's client
// fills (issue 757): the value the provider's software issued for this module on this machine. Name is what the
// module's files call it (`${secret:<name>}`) and Path the file holding it alone.
//
// CredentialForm is the form the provider's offer declares for its credential (Manifest.CredentialFormOf, issue
// 768): the vault makes a first value in that form for a provider it holds no row of. Empty where the provider's
// manifest was not in hand, and the vault then makes nothing.
type PlanProvision struct {
Name string `json:"name,omitempty"`
Provision string `json:"provision"`
Local string `json:"local,omitempty"`
Provider string `json:"provider"`
ProviderModule string `json:"provider-module"`
CredentialForm string `json:"credential-form,omitempty"`
Path string `json:"path,omitempty"`
// Contributes is what the consumer's manifest tells its provider for this provision (Manifest.Contributes: a
// database's name, its extensions), so the provider's helper learns it from the bus (issue 776). Nil where the
// manifest contributes nothing for it, and the entry is then as it was.
Contributes map[string]any `json:"contributes,omitempty"`
// At, From and As are what today's binding file tells the consumer of this provision (issue 788): where the
// provider is on the private network, the machine, and who the consumer is at its end (its slug first), so the
// provider's helper writes them into the consumer's entry and the client fills `${bound:<provision>:at|from|as}`
// from there. Empty where the resolution knew none.
At string `json:"at,omitempty"`
From string `json:"from,omitempty"`
As string `json:"as,omitempty"`
// Names are what the mesh calls the consumer through this provision (issue 792), keyed as its binding file names
// them (`name`, `internal-name`, `name-<local>`, `internal-name-<local>`): composed by the controller, which
// alone knows the node's domains, and carried unchanged by the provider's helper into the consumer's entry. Nil
// where the consumer contributes nothing to the provision, and the entry is then as it was.
Names map[string]string `json:"names,omitempty"`
}
// provisionsOf is, from a resolution's needs, each provision module requires and its provider, by provision then
// local name. A need answered by a record (a licence) or by no module names no provider on a machine and is left
// out: the vault has no provider's value to make for it.
func provisionsOf(needs []Needed, module, as string, contributes map[string]map[string]any,
named map[string]bool) []PlanProvision {
var out []PlanProvision
for _, n := range needs {
if n.For != module || n.ByRecord || n.Module == "" || n.From == "" {
continue
}
p := PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module,
CredentialForm: n.CredentialForm, Contributes: maps.Clone(contributes[n.Name]),
At: n.At, From: n.From, As: as}
// **A credential only a template names** (issue 788): named like a file's, so the client fills it from the
// vault's row for this holder.
if (named[n.Name] || named[SecretLocal(n.Name, n.Local)]) && n.IssuedToHolder {
p.Name = SecretLocal(n.Name, n.Local)
}
out = append(out, p)
}
return out
}
// sortProvisions orders a plan's provisions by provision, then local name.
func sortProvisions(ps []PlanProvision) {
sort.SliceStable(ps, func(i, j int) bool {
if ps[i].Provision != ps[j].Provision {
return ps[i].Provision < ps[j].Provision
}
return ps[i].Local < ps[j].Local
})
}
// PlanSecret is one of the module's own secrets, by its name in the manifest; Path is the file holding it alone,
// when the manifest's own-secrets places one.
type PlanSecret struct {
Name string `json:"name"`
Path string `json:"path,omitempty"`
}
// PlanTemplates is where the templates archive is fetched from on this node, and what it hashes to.
type PlanTemplates struct {
Source string `json:"source"`
Digest string `json:"digest"`
}
// PlanFile is one file the client fills.
type PlanFile struct {
ID string `json:"id"`
Path string `json:"path"`
Template string `json:"template"`
OwnerAtRuntime string `json:"owner-at-runtime"`
Takes string `json:"takes,omitempty"`
}
// The words a placed file says who owns it at runtime with (novox/hq ADR 0349 §5).
const (
RuntimeOwned = "owned"
RuntimeSeed = "seed"
RuntimeManaged = "managed"
)
// OwnerAtRuntimeField is the key a declared file says who owns it at runtime by (the node-engine's
// `owner-at-runtime`).
const OwnerAtRuntimeField = "owner-at-runtime"
// ownershipOf is who owns a file at runtime, by its form, as the node-engine maps the old forms (mesh-host
// declaration.File.Ownership): written into is managed, created once is a seed, any other is owned.
func ownershipOf(r map[string]any) string {
switch {
case r["into"] != nil && fmt.Sprint(r["into"]) != "":
return RuntimeManaged
case r["create-once"] == true:
return RuntimeSeed
}
return RuntimeOwned
}
// takesOf is how the program reading a file takes a change, by what names the file among the module's resources:
// restart-on wins over reload-on; empty when nothing names it (the program reads it when it uses it).
func takesOf(m Manifest, id string) string {
takes := ""
for _, r := range m.Resources {
if slices.Contains(stringsIn(r["restart-on"]), id) {
return "restart-on"
}
if slices.Contains(stringsIn(r["reload-on"]), id) {
takes = "reload-on"
}
}
return takes
}
// movedProblems is what is wrong with a module's mark: a moved module must name a template (as written) or carry
// what its build kept (as held), and Handed is the build's word, never a repository's.
func movedProblems(m Manifest) []string {
switch {
case m.Handed != nil && !m.Moved:
return []string{fmt.Sprintf("%s carries what a moved module's build keeps (handed) and is not marked moved",
m.Module)}
case m.Handed != nil && m.Build != nil:
return []string{fmt.Sprintf("%s says handed in its repository: that is what its build keeps, never "+
"written by hand (novox/hq ADR 0349 §2)", m.Module)}
case m.Moved && m.Handed == nil && !m.Templated():
return []string{fmt.Sprintf("%s is marked moved and names no template: a moved module's files are filled "+
"from template files of its own, `\"template\": \"%s/<name>\"` (novox/hq ADR 0349 §2)", m.Module,
m.TemplatesDir())}
}
if !m.Moved {
return nil
}
// **A moved module's secrets are the vault's, handed to its machine's client** (design 57 V4): a secret the
// mesh makes, by name. A family's members and a value issued outside the mesh are given by a person and may be
// absent, which the client cannot tell from one the vault does not hold yet; they move with a package of their
// own.
var problems []string
if fams := m.OwnSecrets.Families(); len(fams) > 0 {
problems = append(problems, fmt.Sprintf("%s is marked moved and keeps a secret family (%s): a moved module's "+
"secrets are handed by the vault by name, and a family's members are not yet (design 57 V4)", m.Module,
strings.Join(fams, ", ")))
}
for _, name := range sortedKeys(m.OwnSecrets.Plain()) {
if m.OwnSecrets[name].IssuedBy == IssuedOutside {
problems = append(problems, fmt.Sprintf("%s is marked moved and its secret %s is issued outside the mesh: "+
"a value that may not be given is not yet handed by the vault (design 57 V4)", m.Module, name))
}
}
return problems
}
// handedBy is what a moved module's build keeps, from its templates archive as built, or nil for a module not
// moved.
func handedBy(m Manifest, archive Built) *Handed {
if !m.Moved {
return nil
}
dir := m.TemplatesDir()
h := &Handed{Source: Recorded(archive.Reference), Digest: archive.Digest, Templates: map[string]string{}}
for _, r := range m.Resources {
name, has, wrong := templateOf(m.Module, dir, r)
if !has || wrong != "" {
continue
}
h.Templates[fmt.Sprint(r["id"])] = strings.TrimPrefix(name, dir+"/")
}
for _, it := range m.Items {
if it.Template == "" || itemTemplateProblem(m, it.Template) != "" {
continue
}
if h.Items == nil {
h.Items = map[string]string{}
}
h.Items[it.key()] = strings.TrimPrefix(it.Template, dir+"/")
}
return h
}
// handing is whether this composition hands the module's templated files to the provisioner.
func (with Rendering) handing(m Manifest) bool {
return m.Moved && m.Handed != nil && with.Hands && with.Provisioner != ""
}
// Handing is whether a machine composed with this rendering hands the module to the provisioner, its files and
// its own secrets (design 57 P6, V4): a moved module there, assigned or only pulled in by another (issue 765), on a
// machine that can take it. The controller makes and seals none of such a module's secrets for that machine: its
// client asks the vault.
func (with Rendering) Handing(m Manifest) bool { return with.handing(m) }
// requiredByWords begins why a module only pulled in by another is on a machine (Resolution.Because): `required by
// <module>`.
const requiredByWords = "required by "
// RequiredBy is the module that requires module on this machine when nothing assigned it there, only pulled it in
// (issue 765): what its value in the assignments mirror names. Empty for a module assigned there, or not there.
func (r Resolution) RequiredBy(module string) string {
by, _ := strings.CutPrefix(r.Because[module], requiredByWords)
if by == r.Because[module] {
return ""
}
return by
}
// HandedProvisionField marks, between composing a handed module's credential file for something it requires and
// placing it, one the vault keeps for this holder alone (issue 757): its value is the PlanProvision without its path.
// Never on the wire.
const HandedProvisionField = "handed-provision"
// HandedSecretField marks, between composing a module's own secret files and placing them, the one a handing
// composition hands to the provisioner: its value is the secret's name. Never on the wire.
const HandedSecretField = "handed-secret"
// handedSecretFile is a handed module's own secret file as the declaration carries it: placed, the secret's
// alone (0600 unless the manifest says otherwise), owned at runtime, without a value; the client fills it from the
// vault.
func handedSecretFile(r map[string]any, m Manifest, dirs, accessPaths, facts map[string]string,
placed map[string]Placement) (map[string]any, PlanSecret, error) {
name := fmt.Sprint(r[HandedSecretField])
out := map[string]any{}
for k, v := range r {
out[k] = v
}
delete(out, HandedSecretField)
delete(out, "sealed")
if err := dirInto(out, dirs, m.Module); err != nil {
return nil, PlanSecret{}, err
}
if err := accessInto(out, accessPaths, m.Module); err != nil {
return nil, PlanSecret{}, err
}
ownerInto(out, placed)
if err := machineInto(out, facts, m.Module); err != nil {
return nil, PlanSecret{}, err
}
if out["mode"] == nil {
out["mode"] = "0600"
}
out[OwnerAtRuntimeField] = RuntimeOwned
return out, PlanSecret{Name: name, Path: fmt.Sprint(out["path"])}, nil
}
// templatePlaceholder is a placeholder as a template writes it: its namespace and its key.
var templatePlaceholder = regexp.MustCompile(`\$\{([a-z][a-z-]*):([^}\s$]+)\}`)
// configNamespaces are the namespaces the config domain answers: `config`, and `setting`, which the manifest's
// reading writes it as (toSettingsForm).
var configNamespaces = []string{"config", SettingField}
// SettingField is the settings form's word for a config placeholder's namespace.
const SettingField = "setting"
// mergesFile is whether a file resource says `merge` or `takes-settings`: the layers' settings are merged into it.
func mergesFile(r map[string]any) bool {
_, merges := r["merge"]
_, takes := r[TakesSettingsField]
return merges || takes
}
// mergingIn is whether the module's resource id is a merging file (mergesFile).
func mergingIn(m Manifest, id string) bool {
for _, r := range m.Resources {
if fmt.Sprint(r["id"]) == id {
return mergesFile(r)
}
}
return false
}
// handedFile is a moved module's templated file as a handing declaration carries it: placed (its path and owner
// filled by placement alone), saying who owns it at runtime, without its text. And its line of the plan.
func handedFile(r map[string]any, m Manifest, dirs, accessPaths, facts map[string]string, placed map[string]Placement,
with Rendering) (map[string]any, PlanFile, error) {
id := fmt.Sprint(r["id"])
out := map[string]any{}
for k, v := range r {
out[k] = v
}
// **No text, and nothing filled into it**: the client fills it from the template; the declaration says where
// it goes and how it is written.
delete(out, "content")
delete(out, TrustedField)
delete(out, NamesOnPurpose)
delete(out, SecretsInEnvironment)
if err := dirInto(out, dirs, m.Module); err != nil {
return nil, PlanFile{}, err
}
if err := accessInto(out, accessPaths, m.Module); err != nil {
return nil, PlanFile{}, err
}
ownerInto(out, placed)
if err := machineInto(out, facts, m.Module); err != nil {
return nil, PlanFile{}, err
}
out[OwnerAtRuntimeField] = ownershipOf(out)
path := fmt.Sprint(out["path"])
return out, PlanFile{ID: id, Path: path, Template: m.Handed.Templates[id],
OwnerAtRuntime: fmt.Sprint(out[OwnerAtRuntimeField]), Takes: takesOf(m, id)}, nil
}
// placementOf is the value of every placement placeholder the module's handed templates name, keyed as the
// placeholder names it (`machine:account`), each filled by the same pass that fills it in a declaration. A
// template naming a value only another domain answers — a secret, a binding, a contribution — is refused by
// name: the client fills config and placement, and the rest move with their own packages (design 57).
func placementOf(node string, m Manifest, texts, items map[string]string, dirs, accessPaths, facts map[string]string,
with Rendering, moved map[string]bool, backups backupPlacer) (map[string]string, []string, []string, []PlanCollection, error) {
out := map[string]string{}
var secrets, required []string
var problems []string
var collections []PlanCollection
all := map[string]string{}
templateOf := map[string]string{}
for id, text := range texts {
all[id], templateOf[id] = text, m.Handed.Templates[id]
}
for key, text := range items {
all["item:"+key], templateOf["item:"+key] = text, m.Handed.Items[key]
}
judgedConsumers := map[string]bool{}
for _, id := range sortedKeys(all) {
item := strings.HasPrefix(id, "item:")
for _, found := range templatePlaceholder.FindAllStringSubmatch(all[id], -1) {
kind, key := found[1], found[2]
if collection, is := collectionOfPlaceholder(kind, key); is {
// **A collection its file names, read by the client from the items there** (design 57 L1).
comment, err := collectionComment(m, collection)
_, selector := splitSelector(key)
_, selErr := selectorOf(selector)
switch {
case selErr != nil && !item:
problems = append(problems, fmt.Sprintf("%s: its template %s names %s: %v", m.Module,
templateOf[id], found[0], selErr))
case item:
problems = append(problems, fmt.Sprintf("%s: its item's template %s names %s: an item is not a "+
"collector", m.Module, templateOf[id], found[0]))
case err != nil:
problems = append(problems, fmt.Sprintf("%s: its template %s names %s: %v", m.Module,
templateOf[id], found[0], err))
case !slices.ContainsFunc(collections, func(c PlanCollection) bool { return c.Name == collection }):
collections = append(collections, PlanCollection{Name: collection, Comment: comment})
}
continue
}
switch {
case slices.Contains(configNamespaces, kind):
continue
case kind == ConsumersNamespace:
// **A provider's consumers' credentials** (ADR 0373): its machine's client fills them from the vault,
// for a provision the module provides, in a file only it reads (consumersPlaceholderProblems).
if item {
problems = append(problems, fmt.Sprintf("%s: its item's template %s names %s: an item is kept in a "+
"bucket, where no secret is (novox/hq ADR 0349 §7)", m.Module, templateOf[id], found[0]))
} else if !judgedConsumers[id] {
judgedConsumers[id] = true
problems = append(problems, consumersOfTemplate(m, id, all[id])...)
}
continue
case item && (kind == "secret" || kind == "bound"):
problems = append(problems, fmt.Sprintf("%s: its item's template %s names %s: an item is kept in a "+
"bucket, where no secret is (novox/hq ADR 0349 §7)", m.Module, templateOf[id], found[0]))
continue
case kind == "secret":
// **The module's own secret, by name** (design 57 V4): its machine's client asks the vault for it.
// A requirement's credential is a binding's, and moves with the providers (design 57 B1–B3).
if _, own := m.OwnSecrets.Plain()[key]; !own && moved[key] {
// **A credential it requires, from a provider that has moved** (issue 788): the client fills it
// from the vault's row for this holder, which the plan names.
if !slices.Contains(required, key) {
required = append(required, key)
}
} else if !own {
problems = append(problems, fmt.Sprintf("%s: its template %s names %s, which is no secret of its "+
"own; a credential it requires moves with its provider (design 57 B1–B3)", m.Module,
templateOf[id], found[0]))
} else if !slices.Contains(secrets, key) {
secrets = append(secrets, key)
}
continue
case kind == "bound" && moved[strings.SplitN(key, ":", 2)[0]]:
// **What its moved provider told it** (issue 788): the client fills it from the provider's entry.
continue
case !slices.Contains(PlacementKinds, kind):
if known(kind) {
problems = append(problems, fmt.Sprintf("%s: its template %s names %s, and the provisioner's "+
"client fills only config, secrets, collections and placement until that domain moves "+
"(design 57)", m.Module, templateOf[id], found[0]))
}
continue
}
name := kind + ":" + key
if _, done := out[name]; done {
continue
}
if kind == BackupNamespace {
// **What the machine's modules back up** (design 57 L3, issue 794): the text the old contribution
// placeholders render, from the same function, over the modules placed on this machine.
value, problem := backups.placed(m, key, item)
if problem != "" {
problems = append(problems, fmt.Sprintf("%s: its template %s names %s: %s", m.Module,
templateOf[id], found[0], problem))
continue
}
out[name] = value
continue
}
value, err := placed(node, found[0], m, dirs, accessPaths, facts, with)
if err != nil {
problems = append(problems, err.Error())
continue
}
out[name] = value
}
}
if len(problems) > 0 {
sort.Strings(problems)
return nil, nil, nil, nil, errors.New(strings.Join(problems, "; "))
}
sort.Slice(collections, func(a, b int) bool { return collections[a].Name < collections[b].Name })
return out, secrets, required, collections, nil
}
// PlacementKinds are the placeholder namespaces placement answers (ADR 0349 clause 2), as a plan's keys begin.
var PlacementKinds = []string{"dir", "access", "port", "seat", "machine", BackupNamespace}
// known is whether a namespace is one the mesh fills somewhere.
func known(kind string) bool {
for _, f := range fillers {
if f.namespace == kind {
return true
}
}
return false
}
// placed is one placement placeholder's value, filled by the passes that fill it in a file's content.
func placed(node, placeholder string, m Manifest, dirs, accessPaths, facts map[string]string, with Rendering) (string, error) {
r := map[string]any{"id": "placement", "type": "file", "path": "/placement", "content": placeholder}
if err := dirInto(r, dirs, m.Module); err != nil {
return "", err
}
if err := accessInto(r, accessPaths, m.Module); err != nil {
return "", err
}
if err := portInto(r, m.Module, m.Listens, with); err != nil {
return "", err
}
if err := seatInto(r, m.Module, with); err != nil {
return "", err
}
if err := machineInto(r, facts, m.Module); err != nil {
return "", err
}
value := fmt.Sprint(r["content"])
if value == placeholder {
return "", fmt.Errorf("%s: %s is not answered by placement on %s", m.Module, placeholder, node)
}
return value, nil
}
// routedTemplates is the templates archive as this node fetches it, through the artifact store it reaches.
func routedTemplates(m Manifest, with Rendering) (PlanTemplates, error) {
r := map[string]any{"id": "templates", "source": m.Handed.Source}
if err := artifactsInto(r, m.Module, with); err != nil {
return PlanTemplates{}, err
}
return PlanTemplates{Source: fmt.Sprint(r["source"]), Digest: m.Handed.Digest}, nil
}
// handedFiles gathers, over one composition, the files handed to the provisioner and each handed module's plan.
type handedFiles struct {
paths []string
plans map[string]Plan
// secrets is each handed module's own secrets its declaration names, by name: the path of the file holding one
// alone, or "" for one only its templates name.
secrets map[string]map[string]string
// provisions is each handed module's credentials of what it requires that the vault keeps for it (issue 757).
provisions map[string][]PlanProvision
// contributes is each handed module's manifest `contributes`, by the requirement it is about (issue 776).
contributes map[string]map[string]map[string]any
// identities is who each handed module is at its providers' ends (issue 788), and required the credentials of
// what it requires that its templates name.
identities map[string]string
required map[string]map[string]bool
// names is the names composed for each handed module through each requirement it contributes to (issue 792).
names map[string]map[string]map[string]string
}
// name records the names composed for a handed module through one requirement, as its binding file carries them.
func (h *handedFiles) name(module, provision string, own map[string]any) {
values := map[string]string{}
withOwnNames(values, own)
if len(values) == 0 {
return
}
if h.names == nil {
h.names = map[string]map[string]map[string]string{}
}
if h.names[module] == nil {
h.names[module] = map[string]map[string]string{}
}
h.names[module][provision] = values
}
// begin starts a module's plan where this composition hands it, and answers the ids of the files handed: none for
// a module that is not handed here. The plan's templates and placement values are composed now; each file is added
// as it is composed.
//
// **Assigned or only pulled in by another** (issue 765): the plan travels in the module's value of the assignments
// mirror, `<node>.<module>`, which the client reads (inventory.Provisioning); a module pulled in has a value there of
// its own, which says what pulled it in.
func (h *handedFiles) begin(node string, m Manifest, dirs, accessPaths, facts map[string]string,
with Rendering, caps map[string]bool, needs []Needed, backups backupPlacer) (map[string]bool, error) {
if !with.handing(m) {
return nil, nil
}
texts := map[string]string{}
ids := map[string]bool{}
for _, r := range m.Resources {
id := fmt.Sprint(r["id"])
if _, named := m.Handed.Templates[id]; !named {
continue
}
if mergesFile(r) {
// **A merging file is filled the old way** (issue 831, ADR 0349 clause 17): handing it needs the client
// to merge the layers, which nothing builds yet. Its text stays, the controller fills and merges it.
continue
}
if fmt.Sprint(r["type"]) != "file" {
return nil, fmt.Errorf("%s: %s is a %v and its build kept a template for it; only a file is filled "+
"from one", m.Module, id, r["type"])
}
text, _ := r["content"].(string)
texts[id] = text
ids[id] = true
}
for _, id := range sortedKeys(m.Handed.Templates) {
if !ids[id] && !mergingIn(m, id) {
return nil, fmt.Errorf("%s: its build kept a template for %s, which it does not declare", m.Module, id)
}
}
// **Its items, by template** (design 57 L1): the client fills each and records it for this node.
items := map[string]string{}
var planned []PlanItem
for _, it := range m.Items {
template, kept := m.Handed.Items[it.key()]
if !kept {
return nil, fmt.Errorf("%s: its build kept no template for its item of %s", m.Module, it.key())
}
// **A capability the machine lacks leaves the item out of its plan** (design 57 L3): the client judges none.
if it.IfCapability != "" && !caps[it.IfCapability] {
continue
}
items[it.key()] = it.Content
planned = append(planned, PlanItem{Collection: it.Collection, Name: it.Name,
Attributes: it.Attributes, Template: template})
}
if len(ids) == 0 && len(planned) == 0 {
return nil, nil
}
templates, err := routedTemplates(m, with)
if err != nil {
return nil, err
}
// What it requires from a provider that has moved (issue 788): its templates may name what that provider tells it.
moved := map[string]bool{}
for _, n := range needs {
if n.For == m.Module && n.ProviderMoved {
moved[n.Name] = true
// A credential kept per local name (issue 793) is named by its local in a template.
moved[SecretLocal(n.Name, n.Local)] = true
}
}
placement, secrets, required, collections, err := placementOf(node, m, texts, items, dirs, accessPaths, facts,
with, moved, backups)
if err != nil {
return nil, err
}
if h.identities == nil {
h.identities = map[string]string{}
h.required = map[string]map[string]bool{}
}
h.identities[m.Module] = ConsumerIdentity(node, IdentitySource(m.Slug, m.Module))
h.required[m.Module] = map[string]bool{}
for _, name := range required {
h.required[m.Module][name] = true
}
for _, name := range secrets {
h.secret(m.Module, PlanSecret{Name: name})
}
plan := Plan{Templates: templates, Items: planned, Collections: collections}
if len(placement) > 0 {
plan.Placement = placement
}
h.plans[m.Module] = plan
if len(m.Contributes) > 0 {
if h.contributes == nil {
h.contributes = map[string]map[string]map[string]any{}
}
h.contributes[m.Module] = m.Contributes
}
return ids, nil
}
// add is one handed file of a module, composed. A path handed twice is refused: the node-engine writes a filled
// file for exactly one resource, and the client's write names only the path (mesh-host internal/apply).
func (h *handedFiles) add(module string, f PlanFile) error {
if slices.Contains(h.paths, f.Path) {
return fmt.Errorf("%s: %s is handed to the provisioner twice on this machine; a filled file has one "+
"resource and one writer (novox/hq ADR 0349 §5)", module, f.Path)
}
plan := h.plans[module]
plan.Files = append(plan.Files, f)
h.plans[module] = plan
h.paths = append(h.paths, f.Path)
return nil
}
// secret is one of a handed module's own secrets its declaration names; a file holding it alone is a filled file.
func (h *handedFiles) secret(module string, sec PlanSecret) {
if h.secrets == nil {
h.secrets = map[string]map[string]string{}
}
if h.secrets[module] == nil {
h.secrets[module] = map[string]string{}
}
if sec.Path != "" || h.secrets[module][sec.Name] == "" {
h.secrets[module][sec.Name] = sec.Path
}
}
// addSecretFile is a handed module's own secret file, composed: a filled file, and its secret in the plan.
func (h *handedFiles) addSecretFile(module string, sec PlanSecret) error {
if slices.Contains(h.paths, sec.Path) {
return fmt.Errorf("%s: %s is handed to the provisioner twice on this machine; a filled file has one "+
"resource and one writer (novox/hq ADR 0349 §5)", module, sec.Path)
}
h.paths = append(h.paths, sec.Path)
h.secret(module, sec)
return nil
}
// addProvisionFile is a handed module's credential file for something it requires, which the vault keeps for it
// alone (issue 757): a filled file, and the credential in the plan.
func (h *handedFiles) addProvisionFile(module string, pr PlanProvision) error {
if slices.Contains(h.paths, pr.Path) {
return fmt.Errorf("%s: %s is handed to the provisioner twice on this machine; a filled file has one "+
"resource and one writer (novox/hq ADR 0349 §5)", module, pr.Path)
}
h.paths = append(h.paths, pr.Path)
if h.provisions == nil {
h.provisions = map[string][]PlanProvision{}
}
h.provisions[module] = append(h.provisions[module], pr)
return nil
}
// finished is every plan with its secrets, by name, and what its module requires, each with its provider.
func (h *handedFiles) finished(needs []Needed) map[string]Plan {
if len(h.plans) == 0 {
return nil
}
for module, plan := range h.plans {
// **Every provision it requires, with its provider** (issue 760); a credential the vault keeps for it alone
// is that provision's entry, with its name and file (issue 757).
provisions := provisionsOf(needs, module, h.identities[module], h.contributes[module], h.required[module])
for _, credential := range h.provisions[module] {
i := slices.IndexFunc(provisions, func(p PlanProvision) bool {
return p.Provision == credential.Provision && p.Local == credential.Local
})
if i < 0 {
credential.As = h.identities[module]
provisions = append(provisions, credential)
continue
}
provisions[i].Name, provisions[i].Path = credential.Name, credential.Path
}
for i := range provisions {
provisions[i].Names = maps.Clone(h.names[module][provisions[i].Provision])
}
sortProvisions(provisions)
plan.Provisions = provisions
h.plans[module] = plan
}
for module, named := range h.secrets {
plan, ok := h.plans[module]
if !ok {
continue
}
plan.Secrets = nil
for _, name := range sortedKeys(named) {
plan.Secrets = append(plan.Secrets, PlanSecret{Name: name, Path: named[name]})
}
h.plans[module] = plan
}
return h.plans
}