ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
666 lines
27 KiB
Go
666 lines
27 KiB
Go
package resolution
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"maps"
|
||
"regexp"
|
||
"slices"
|
||
"sort"
|
||
"strings"
|
||
|
||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||
)
|
||
|
||
// EncodeContributed is f as the bytes PlanProvision.Contributed carries, refused where DecodeContributed would refuse
|
||
// them, so a plan never carries what its reader cannot take.
|
||
func EncodeContributed(f mcat.ContributedForm) (json.RawMessage, error) {
|
||
f.V = mcat.ContributedVersion
|
||
raw, err := json.Marshal(f)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := mcat.DecodeContributed(raw); err != nil {
|
||
return nil, err
|
||
}
|
||
return raw, nil
|
||
}
|
||
|
||
// 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 []mcat.Needed, module string, handed bool, as string, contributes map[string]map[string]any,
|
||
named map[string]bool) []mcat.PlanProvision {
|
||
var out []mcat.PlanProvision
|
||
for _, n := range needs {
|
||
// **The one rule** (Keyed, issue 774): what the seat keys and the want waits for.
|
||
if n.For != module || !Keyed(handed, n) {
|
||
continue
|
||
}
|
||
p := mcat.PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module,
|
||
CredentialForm: n.CredentialForm, CredentialTool: n.CredentialTool, 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[mcat.SecretLocal(n.Name, n.Local)]) && n.IssuedToHolder {
|
||
p.Name = mcat.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 []mcat.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
|
||
})
|
||
}
|
||
|
||
// 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 mcat.RuntimeManaged
|
||
case r["create-once"] == true:
|
||
return mcat.RuntimeSeed
|
||
}
|
||
return mcat.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 mcat.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
|
||
}
|
||
|
||
// handing is whether this composition hands the module's templated files to the provisioner: handed (Handing), with
|
||
// the templates its build kept. A module the vault judged handed whose build kept none is refused where its secrets
|
||
// are placed ("nothing would fill it"), never composed with values the vault does not answer.
|
||
func renderingHanding(with mcat.Rendering, m mcat.Manifest) bool {
|
||
return Handing(with, m) && m.Handed != nil
|
||
}
|
||
|
||
// 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.
|
||
//
|
||
// **Where the vault judged the module, its word is the rule** (Rendering.Assigned, issue 774): the vault leaves a
|
||
// module's own secrets out of `own` exactly when the plan in its assignment hands it (AssignmentHands), so a node
|
||
// composing itself hands exactly those, whatever its own reading of the manifest and the machine says. On 2026-10-31
|
||
// postgres on novox was held: its plan handed its secrets, the vault answered none of them, and the node, judging by
|
||
// the manifest and the machine, composed them itself and refused for want of `reader`. Two readers, one rule.
|
||
func Handing(with mcat.Rendering, m mcat.Manifest) bool {
|
||
if hands, judged := with.Assigned[m.Module]; judged {
|
||
return hands
|
||
}
|
||
return m.Moved && m.Handed != nil && with.Hands && with.Provisioner != ""
|
||
}
|
||
|
||
// 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 RequiredBy(r mcat.Resolution, module string) string {
|
||
by, _ := strings.CutPrefix(r.Because[module], requiredByWords)
|
||
if by == r.Because[module] {
|
||
return ""
|
||
}
|
||
return by
|
||
}
|
||
|
||
// 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 mcat.Manifest, dirs, accessPaths, facts map[string]string,
|
||
placed map[string]mcat.Placement) (map[string]any, mcat.PlanSecret, error) {
|
||
name := fmt.Sprint(r[mcat.HandedSecretField])
|
||
out := map[string]any{}
|
||
for k, v := range r {
|
||
out[k] = v
|
||
}
|
||
delete(out, mcat.HandedSecretField)
|
||
delete(out, "sealed")
|
||
if err := dirInto(out, dirs, m.Module); err != nil {
|
||
return nil, mcat.PlanSecret{}, err
|
||
}
|
||
if err := accessInto(out, accessPaths, m.Module); err != nil {
|
||
return nil, mcat.PlanSecret{}, err
|
||
}
|
||
ownerInto(out, placed)
|
||
if err := machineInto(out, facts, m.Module); err != nil {
|
||
return nil, mcat.PlanSecret{}, err
|
||
}
|
||
if out["mode"] == nil {
|
||
out["mode"] = "0600"
|
||
}
|
||
out[mcat.OwnerAtRuntimeField] = mcat.RuntimeOwned
|
||
return out, mcat.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", mcat.SettingField}
|
||
|
||
// 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[mcat.TakesSettingsField]
|
||
return merges || takes
|
||
}
|
||
|
||
// mergingIn is whether the module's resource id is a merging file (mergesFile).
|
||
func mergingIn(m mcat.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 mcat.Manifest, dirs, accessPaths, facts map[string]string, placed map[string]mcat.Placement,
|
||
with mcat.Rendering) (map[string]any, mcat.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, mcat.TrustedField)
|
||
delete(out, mcat.NamesOnPurpose)
|
||
delete(out, mcat.SecretsInEnvironment)
|
||
if err := dirInto(out, dirs, m.Module); err != nil {
|
||
return nil, mcat.PlanFile{}, err
|
||
}
|
||
if err := accessInto(out, accessPaths, m.Module); err != nil {
|
||
return nil, mcat.PlanFile{}, err
|
||
}
|
||
ownerInto(out, placed)
|
||
if err := machineInto(out, facts, m.Module); err != nil {
|
||
return nil, mcat.PlanFile{}, err
|
||
}
|
||
out[mcat.OwnerAtRuntimeField] = ownershipOf(out)
|
||
path := fmt.Sprint(out["path"])
|
||
return out, mcat.PlanFile{ID: id, Path: path, Template: m.Handed.Templates[id],
|
||
OwnerAtRuntime: fmt.Sprint(out[mcat.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 mcat.Manifest, texts, items map[string]string, dirs, accessPaths, facts map[string]string,
|
||
with mcat.Rendering, moved map[string]bool, backups backupPlacer) (map[string]string, []string, []string, []mcat.PlanCollection, error) {
|
||
out := map[string]string{}
|
||
var secrets, required []string
|
||
var problems []string
|
||
var collections []mcat.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 mcat.PlanCollection) bool { return c.Name == collection }):
|
||
collections = append(collections, mcat.PlanCollection{Name: collection, Comment: comment})
|
||
}
|
||
continue
|
||
}
|
||
switch {
|
||
case slices.Contains(configNamespaces, kind):
|
||
continue
|
||
case kind == mcat.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(mcat.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 == mcat.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
|
||
}
|
||
|
||
// 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 mcat.Manifest, dirs, accessPaths, facts map[string]string, with mcat.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 mcat.Manifest, with mcat.Rendering) (mcat.PlanTemplates, error) {
|
||
r := map[string]any{"id": "templates", "source": m.Handed.Source}
|
||
if err := artifactsInto(r, m.Module, with); err != nil {
|
||
return mcat.PlanTemplates{}, err
|
||
}
|
||
return mcat.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]mcat.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][]mcat.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
|
||
// keyedOnly is each module with a plan only for what Keyed keys of a consumer not handed: a provision its
|
||
// provider's helper answers (issue 774), such as a route.
|
||
keyedOnly map[string]bool
|
||
// contributed is what each handed module gives each requirement's provider, encoded (PlanProvision.Contributed).
|
||
contributed map[string]map[string]json.RawMessage
|
||
}
|
||
|
||
// contribute records what a handed module gives one requirement's provider, encoded; nothing for none.
|
||
func (h *handedFiles) contribute(module, provision string, raw json.RawMessage) {
|
||
if len(raw) == 0 {
|
||
return
|
||
}
|
||
if h.contributed == nil {
|
||
h.contributed = map[string]map[string]json.RawMessage{}
|
||
}
|
||
if h.contributed[module] == nil {
|
||
h.contributed[module] = map[string]json.RawMessage{}
|
||
}
|
||
h.contributed[module][provision] = raw
|
||
}
|
||
|
||
// keyed starts the plan of a module begin started none for, when the composition keys any of its needs as a consumer
|
||
// not handed (Keyed, issue 774): one whose provider's helper answers a provision of it (grafana's route). A module
|
||
// handed that keeps no template or item (a held one's stub, issue 1126) is not handed here either: what it keeps
|
||
// stays the controller's but for what a helper answers. Its plan names provisions alone: no templates, files or
|
||
// secrets. Answers whether it started one.
|
||
func (h *handedFiles) keyed(node string, m mcat.Manifest, needs []mcat.Needed) bool {
|
||
if _, started := h.plans[m.Module]; started {
|
||
return false
|
||
}
|
||
if !slices.ContainsFunc(needs, func(n mcat.Needed) bool { return n.For == m.Module && Keyed(false, n) }) {
|
||
return false
|
||
}
|
||
if h.identities == nil {
|
||
h.identities = map[string]string{}
|
||
h.required = map[string]map[string]bool{}
|
||
}
|
||
h.identities[m.Module] = mcat.ConsumerIdentity(node, mcat.IdentitySource(m.Slug, m.Module))
|
||
h.plans[m.Module] = mcat.Plan{}
|
||
if h.keyedOnly == nil {
|
||
h.keyedOnly = map[string]bool{}
|
||
}
|
||
h.keyedOnly[m.Module] = true
|
||
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 true
|
||
}
|
||
|
||
// 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 mcat.Manifest, dirs, accessPaths, facts map[string]string,
|
||
with mcat.Rendering, caps map[string]bool, needs []mcat.Needed, backups backupPlacer) (map[string]bool, error) {
|
||
if !renderingHanding(with, 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 []mcat.PlanItem
|
||
for _, it := range m.Items {
|
||
template, kept := m.Handed.Items[itemKey(it)]
|
||
if !kept {
|
||
return nil, fmt.Errorf("%s: its build kept no template for its item of %s", m.Module, itemKey(it))
|
||
}
|
||
// **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[itemKey(it)] = it.Content
|
||
planned = append(planned, mcat.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[mcat.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] = mcat.ConsumerIdentity(node, mcat.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, mcat.PlanSecret{Name: name})
|
||
}
|
||
plan := mcat.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 mcat.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 mcat.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 mcat.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 mcat.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][]mcat.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 []mcat.Needed) map[string]mcat.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.keyedOnly[module], h.identities[module], h.contributes[module],
|
||
h.required[module])
|
||
for _, credential := range h.provisions[module] {
|
||
i := slices.IndexFunc(provisions, func(p mcat.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)
|
||
// **What the consumer contributes, on the provision's first entry** (issue 774): a provider serving from the
|
||
// plans alone reads each contribution once, however many local names the requirement has.
|
||
gave := map[string]bool{}
|
||
for i := range provisions {
|
||
if raw := h.contributed[module][provisions[i].Provision]; len(raw) > 0 && !gave[provisions[i].Provision] {
|
||
gave[provisions[i].Provision] = true
|
||
provisions[i].Contributed = slices.Clone(raw)
|
||
}
|
||
}
|
||
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, mcat.PlanSecret{Name: name, Path: named[name]})
|
||
}
|
||
h.plans[module] = plan
|
||
}
|
||
return h.plans
|
||
}
|