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 // `. 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, `.`, 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 }