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 `/` 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:}`) 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::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-`, `internal-name-`): 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/\"` (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 // `. 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, `.`, 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 }