800 lines
35 KiB
Go
800 lines
35 KiB
Go
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
|
||
}
|