Files
mesh-host/internal/compose/resolution/handed.go
T
jschoubben d68e241724 Take the catalogue's composition rules into the node-engine
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
2026-11-02 19:15:55 +01:00

666 lines
27 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 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
}