Files
mesh-controller/internal/catalogue/consumers.go
T
mesh-tools efc5f901af
mesh/merge-gate head efc5f901afb7 merged into trunk cf897e89fe31 — pass: [release feat/57-config-and-provisioning, judged by its controller 93b947cc53b5] t…
mesh/repo-check head efc5f901afb7 merged into trunk cf897e89fe31 — pass: [release feat/57-config-and-provisioning, judged by its controller 93b947cc53b5] i…
mesh/delivery delivered
Judge a moved module in the merge gate as a real build hands it its templates, so the gate no longer refuses every moved module (issue 876)
2026-10-16 08:08:11 +00:00

111 lines
5.4 KiB
Go

package catalogue
import (
"fmt"
"regexp"
"slices"
)
// A provider's consumers' credentials, in a file of the provider's own (novox/hq ADR 0373, issue 679).
//
// A provider in the tool runner may not ask the vault (ADR 0349 clause 11, ADR 0370 clause 2), so its template names
// `${consumers:<provision>}` and its machine's provisioner client fills it: one entry per consumer bound to the
// provider for that provision, each carrying the credential the vault keeps for the pair. Only the client fills it,
// so the placeholder is a provider's alone, for a provision it provides, in a file only the provider reads (mode 0600
// or 0400, owned at runtime, so a change on disk is written back); and a module not handed to the client on a machine
// is refused there, never written the placeholder's text, nor an empty list the provider would read as no consumers.
// ConsumersNamespace is a provider's consumers' credentials' placeholder namespace, `${consumers:<provision>}`.
const ConsumersNamespace = "consumers"
// ofConsumers is where a provider places its consumers' credentials. Loose inside the braces, so a misspelt name is
// refused rather than passed over.
var ofConsumers = regexp.MustCompile(`\$\{consumers:([^}]*)\}`)
// consumersPlaceholderProblems is what is wrong with one resource's ${consumers:…}: placed only in a file's content,
// for a provision the module provides, in a file only the provider reads and the client writes back.
func consumersPlaceholderProblems(m Manifest, r map[string]any) []string {
var problems []string
for _, field := range sortedKeys(r) {
v, ok := r[field].(string)
if !ok {
continue
}
for _, f := range ofConsumers.FindAllStringSubmatch(v, -1) {
switch {
case field != "content" || fmt.Sprint(r["type"]) != "file":
problems = append(problems, fmt.Sprintf("%s's resource %v names %s in its %s; a provider's consumers "+
"are placed only in a file's content (novox/hq ADR 0373)", m.Module, r["id"], f[0], field))
continue
case !slices.ContainsFunc(m.Provides, func(o Offer) bool { return o.Name == f[1] }):
problems = append(problems, fmt.Sprintf("%s's resource %v names %s, and %s does not provide %q: only a "+
"provider's template holds its consumers' credentials (novox/hq ADR 0373)", m.Module, r["id"], f[0],
m.Module, f[1]))
}
if mode := fmt.Sprint(r["mode"]); mode != "0600" && mode != "0400" {
problems = append(problems, fmt.Sprintf("%s's resource %v holds its consumers' credentials (%s) with "+
"mode %q: the provider alone reads it, 0600 or 0400 (novox/hq ADR 0373)", m.Module, r["id"], f[0],
r["mode"]))
}
if owner := ownershipOf(r); owner != RuntimeOwned {
problems = append(problems, fmt.Sprintf("%s's resource %v holds its consumers' credentials (%s) and is "+
"%s at runtime: it is owned, written back whenever it differs (novox/hq ADR 0373)", m.Module,
r["id"], f[0], owner))
}
}
}
return problems
}
// consumersHandedOnly says what a file naming ${consumers:…} is where it would be written the old way: the provisioner's
// client alone fills it, so on a machine that does not hand the module it is **left out** of the declaration, never
// written as that text or as an empty list the provider would read as no consumers, and never the reason the module is
// refused: a provider not handed there keeps working as it did, from its grant files (novox/hq ADR 0373, issue 780).
// A moved module judged from its repository's tree (WithTemplatesFrom: `module check`), on a machine
// that would hand it, has no templates archive to hand and is judged as it composes; there the placeholder is judged
// and taken out of the text, since what is composed so is never sent (judged answers whether the file is so judged).
func consumersHandedOnly(m Manifest, r map[string]any, with Rendering) (judged, leftOut bool) {
content, _ := r["content"].(string)
switch {
case ofConsumers.FindString(content) == "":
return false, false
case m.Moved && m.Handed == nil && with.Hands && with.Provisioner != "":
return true, false
}
return false, true
}
// consumersLeftOutLine is the plan's one line for a file left out because the module is not handed.
func consumersLeftOutLine(m Manifest, r map[string]any) string {
return fmt.Sprintf("%s's %v names ${consumers:…}, which the provisioner's client alone fills: left out, handed only "+
"(novox/hq ADR 0373)", m.Module, r["id"])
}
// consumersNotHanded is consumersHandedOnly as a refusal, for a caller that has no way to leave the file out.
func consumersNotHanded(m Manifest, r map[string]any, with Rendering) (judged bool, err error) {
judged, left := consumersHandedOnly(m, r, with)
if left {
return false, fmt.Errorf("%s's resource %v names %s, which the provisioner's client alone fills, and %s is not "+
"handed to it on this machine: it would be written as that text, or empty, and the provider would read no "+
"consumers (novox/hq ADR 0373)", m.Module, r["id"], ofConsumers.FindString(fmt.Sprint(r["content"])), m.Module)
}
return judged, nil
}
// consumersOfTemplate is what is wrong with a handed file's template naming ${consumers:…}, judged on the file's
// resource as if its text were inline.
func consumersOfTemplate(m Manifest, id, text string) []string {
for _, r := range m.Resources {
if fmt.Sprint(r["id"]) != id {
continue
}
withText := map[string]any{}
for k, v := range r {
withText[k] = v
}
withText["content"] = text
return consumersPlaceholderProblems(m, withText)
}
return nil
}