Merge pull request 'One rule for which provisions a module waits for, so the assigner, the plans and the provisioner agree and grafana gets its route (issue 774)' (#143) from fix/one-rule-for-provisions into main
This commit was merged in pull request #143.
This commit is contained in:
@@ -1065,9 +1065,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// **What the mesh calls the module through each provision it requires** (issue 792): the names its binding
|
||||
// file carries, composed by the same call, handed to the provider through the plan's entry.
|
||||
if _, ok := handed.plans[m.Module]; ok && handing != nil {
|
||||
// **A plan for what the seat keys of it all the same** (catalogue.Keyed, issue 774): a module not handed whose
|
||||
// provider's helper answers a provision of it, its route the commonest, has that provision in a plan of its own,
|
||||
// so the seat keys it and the helper hears it.
|
||||
keyedOnly := handing == nil && handed.keyed(r.Node, m, r.Needs)
|
||||
if _, ok := handed.plans[m.Module]; ok && (handing != nil || keyedOnly) {
|
||||
for _, to := range requirementsContributedTo(m) {
|
||||
own, err := r.ownNames(m, to, with.Settings[m.Module])
|
||||
if err != nil {
|
||||
|
||||
+39
-3
@@ -129,11 +129,12 @@ type PlanProvision struct {
|
||||
// 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,
|
||||
func provisionsOf(needs []Needed, module string, handed bool, 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 == "" {
|
||||
// **The one rule** (Keyed, issue 774): what the seat keys and the want waits for.
|
||||
if n.For != module || !Keyed(handed, n) {
|
||||
continue
|
||||
}
|
||||
p := PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module,
|
||||
@@ -589,6 +590,40 @@ type handedFiles struct {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 Manifest, needs []Needed) bool {
|
||||
if _, started := h.plans[m.Module]; started {
|
||||
return false
|
||||
}
|
||||
if !slices.ContainsFunc(needs, func(n 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] = ConsumerIdentity(node, IdentitySource(m.Slug, m.Module))
|
||||
h.plans[m.Module] = 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.
|
||||
@@ -768,7 +803,8 @@ func (h *handedFiles) finished(needs []Needed) map[string]Plan {
|
||||
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])
|
||||
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 PlanProvision) bool {
|
||||
return p.Provision == credential.Provision && p.Local == credential.Local
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package catalogue
|
||||
|
||||
import "slices"
|
||||
|
||||
// The one rule of which provisions the seat mesh-provisioner keys and a want waits for (novox/hq ADR 0570 §1, §2, §4;
|
||||
// issue 774). The plan composition puts a need in a consumer's plan by it, the holder of mesh-provisioner keys what the
|
||||
// plans name, and the assigner's rule 8 waits for what Awaited answers: three parts, one rule, so they cannot drift
|
||||
// again (on 2026-10-30 the assigner waited for every `requires` while the plans named only some, and every new build of
|
||||
// grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited for a key nobody would make).
|
||||
//
|
||||
// A need a module answers (not a record, not nothing) is keyed when its consumer is handed to the provisioner, or the
|
||||
// provider's offer says the provider helper answers it. A need of a consumer not handed whose provider's helper does
|
||||
// not answer it is still answered by the controller's composition: it is neither keyed nor waited for.
|
||||
|
||||
// HandedByManifest is whether a module is handed to the provisioner as far as its manifest says (Rendering.Handing
|
||||
// without the machine's own words, and begin's): marked moved, not a held module's stub, with a template its build
|
||||
// kept or an item.
|
||||
func (m Manifest) HandedByManifest() bool {
|
||||
return m.Moved && m.Handed != nil && !m.Stubbed && (len(m.Handed.Templates) > 0 || len(m.Items) > 0)
|
||||
}
|
||||
|
||||
// Keyed is whether a need is a provision of the seat mesh-provisioner: a key, an entry in its consumer's plan, and a
|
||||
// wait of the consumer's want. consumerHanded is whether the composition hands the consumer to the provisioner.
|
||||
func Keyed(consumerHanded bool, n Needed) bool {
|
||||
if n.ByRecord || n.Module == "" || n.From == "" {
|
||||
return false
|
||||
}
|
||||
return consumerHanded || n.AnsweredByHelper
|
||||
}
|
||||
|
||||
// Awaited is, of what consumer requires (not optionally), each provision Keyed answers yes for, judged from manifests
|
||||
// alone, as the assigner reads them: providers answers the modules providing a provision. A provision is a need when a
|
||||
// provider offers it to the mesh, or one on the consumer's machine serves facts about it (Resolve: one answered on the
|
||||
// machine with nothing served binds nothing). It is keyed when the consumer is handed, or every provider's offer is
|
||||
// answered by the helper: where the providers disagree, which one answers is the composition's, and nothing is waited
|
||||
// for that might never be keyed. Sorted, each once.
|
||||
func Awaited(consumer Manifest, providers func(provision string) []Manifest) []string {
|
||||
var out []string
|
||||
for _, r := range consumer.Requires {
|
||||
if consumer.OptionalRequires[r] || slices.Contains(out, r) {
|
||||
continue
|
||||
}
|
||||
by := providers(r)
|
||||
if len(by) == 0 {
|
||||
continue
|
||||
}
|
||||
need, helper := false, true
|
||||
for _, p := range by {
|
||||
for _, o := range p.Provides {
|
||||
if o.Name != r {
|
||||
continue
|
||||
}
|
||||
if o.At() == ScopeMesh || len(p.Serves[r]) > 0 {
|
||||
need = true
|
||||
}
|
||||
}
|
||||
if !p.AnsweredByHelperFor(r) {
|
||||
helper = false
|
||||
}
|
||||
}
|
||||
if need && (consumer.HandedByManifest() || helper) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The one rule replays the live cases of 2026-10-30 (issue 774): what the assigner's rule 8 waits for is what the
|
||||
// composition keys, judged from the manifests alone.
|
||||
func TestAwaitedIsWhatTheCompositionKeys(t *testing.T) {
|
||||
built := &Handed{Source: "store://t", Digest: "sha256:t", Templates: map[string]string{"c": "c"}}
|
||||
offer := func(module, name string, scope string, helper bool, serves bool) Manifest {
|
||||
o := Offer{Name: name, Scope: scope}
|
||||
if helper {
|
||||
o.AnsweredBy = AnsweredByHelper
|
||||
}
|
||||
m := Manifest{Module: module, Provides: []Offer{o}}
|
||||
if serves {
|
||||
m.Serves = map[string]map[string]any{name: {"port": 1}}
|
||||
}
|
||||
return m
|
||||
}
|
||||
gitea, distribution := offer("gitea", "forge-account", ScopeMesh, false, false), offer("distribution", "artifact-store", ScopeMesh, false, false)
|
||||
goTools, nodeJS := offer("go", "go-toolchain", ScopeNode, false, false), offer("nodejs", "node-toolchain", ScopeNode, false, false)
|
||||
keycloak, proxy := offer("keycloak", "oidc-client", ScopeMesh, false, false), offer("route-proxy", "route", ScopeMesh, true, false)
|
||||
adapter := offer("route-adapter", "route", ScopeMesh, false, false)
|
||||
ollama := offer("ollama", "llm", ScopeNode, false, true)
|
||||
by := func(ms ...Manifest) func(string) []Manifest {
|
||||
return func(p string) []Manifest {
|
||||
var out []Manifest
|
||||
for _, m := range ms {
|
||||
if slices.Contains(m.Offers(), p) {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
consumer Manifest
|
||||
providers func(string) []Manifest
|
||||
want []string
|
||||
}{
|
||||
{"a handed builder waits for what binds, not for a toolchain on its own machine (mesh-build-go on ace)",
|
||||
Manifest{Module: "mesh-build-go", Moved: true, Handed: built,
|
||||
Requires: []string{"forge-account", "go-toolchain", "artifact-store", "node-tools"}},
|
||||
by(gitea, distribution, goTools), []string{"artifact-store", "forge-account"}},
|
||||
{"node-toolchain alike (mesh-build-node)",
|
||||
Manifest{Module: "mesh-build-node", Moved: true, Handed: built, Requires: []string{"node-toolchain", "forge-account"}},
|
||||
by(gitea, nodeJS), []string{"forge-account"}},
|
||||
{"a module not handed waits for its route, which the helper answers, and not for its login the controller still makes (grafana on ace)",
|
||||
Manifest{Module: "grafana", Requires: []string{"route", "oidc-client", "influxdb-api"},
|
||||
OptionalRequires: map[string]bool{"influxdb-api": true}},
|
||||
by(keycloak, proxy), []string{"route"}},
|
||||
{"where providers disagree on the helper, nothing is waited for that might never be keyed",
|
||||
Manifest{Module: "grafana", Requires: []string{"route"}}, by(proxy, adapter), nil},
|
||||
{"a moved module with no template is not handed (a held stub, issue 1126)",
|
||||
Manifest{Module: "bot", Moved: true, Handed: &Handed{}, Requires: []string{"oidc-client", "route"}},
|
||||
by(keycloak, proxy), []string{"route"}},
|
||||
{"a provision on its own machine that serves facts binds, and is keyed for a handed consumer",
|
||||
Manifest{Module: "chat", Moved: true, Handed: built, Requires: []string{"llm", "llm"}}, by(ollama), []string{"llm"}},
|
||||
} {
|
||||
if got := Awaited(c.consumer, c.providers); !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("%s: awaited %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A module not handed has a plan for what a provider's helper answers of it, and for nothing else (issue 774: grafana
|
||||
// had no plan, so no route key, and route-proxy served no route for it); a need the controller still answers stays out.
|
||||
func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
|
||||
consumer := notifierConsumer(t, false, false)
|
||||
consumer.Requires = append(consumer.Requires, "route")
|
||||
consumer.Contributes = map[string]map[string]any{"route": {"host": "notify.example.org"}}
|
||||
node := workstation()
|
||||
node.At = "workstation.internal"
|
||||
proxy := Manifest{Module: "proxy", Version: "1", Provides: []Offer{{Name: "route", Scope: ScopeMesh, AnsweredBy: AnsweredByHelper}}}
|
||||
store := Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh,
|
||||
Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}}
|
||||
w := twoStores("anchor")
|
||||
w.Offered["route"] = []Provider{{Node: "anchor", At: "anchor.internal", Module: "proxy"}}
|
||||
r, err := Resolve(shelf(consumer, proxy, store), []string{consumer.Module}, node, w)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "op"
|
||||
for i, n := range r.Needs {
|
||||
// As the plan sets it (compose's answeredByHelper).
|
||||
r.Needs[i].AnsweredByHelper = n.Name == "route"
|
||||
}
|
||||
_, before := composeWith(t, r, Rendering{})
|
||||
c, got := composeWith(t, r, handingTo("store:5000"))
|
||||
if got != before {
|
||||
t.Fatalf("the plan changed what is composed:\nbefore: %s\nnow: %s", before, got)
|
||||
}
|
||||
plan, ok := c.Plans["notify"]
|
||||
if !ok || len(plan.Provisions) != 1 || plan.Provisions[0].Provision != "route" || plan.Provisions[0].ProviderModule != "proxy" {
|
||||
t.Fatalf("the plan is %+v (%v), want the route alone", plan, ok)
|
||||
}
|
||||
if plan.Templates != (PlanTemplates{}) || plan.Files != nil || plan.Secrets != nil ||
|
||||
!strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" {
|
||||
t.Fatalf("the plan names %+v", plan)
|
||||
}
|
||||
if want := Awaited(consumer, func(p string) []Manifest {
|
||||
return map[string][]Manifest{"route": {proxy}, "postgres-database": {store}}[p]
|
||||
}); !reflect.DeepEqual(want, []string{"route"}) {
|
||||
t.Fatalf("the assigner waits for %v, and the plan keys the route alone", want)
|
||||
}
|
||||
}
|
||||
@@ -263,6 +263,10 @@ type Needed struct {
|
||||
// then name `${bound:<provision>:<key>}` and `${secret:<provision>}`, which its machine's client fills from the
|
||||
// provider's entry and the vault. Set by the plan, which reads the provider's manifest.
|
||||
ProviderMoved bool
|
||||
// AnsweredByHelper is set when the providing module's offer says the provider helper answers it (issue 792): a
|
||||
// provision the seat mesh-provisioner keys whether or not its consumer is handed (Keyed). Set by the plan, which
|
||||
// reads the provider's manifest.
|
||||
AnsweredByHelper bool
|
||||
}
|
||||
|
||||
// Refusal is why a set of assignments cannot become a declaration.
|
||||
|
||||
@@ -54,6 +54,7 @@ func (c *composer) planFor(nodeName string) (Resolved, error) {
|
||||
// Issued for this holder alone and kept by the vault (issue 757).
|
||||
resolved.Needs[i].IssuedToHolder = issuedToHolder(shelf, n)
|
||||
resolved.Needs[i].ProviderMoved = providerMoved(shelf, n)
|
||||
resolved.Needs[i].AnsweredByHelper = answeredByHelper(shelf, n)
|
||||
if n.ByRecord {
|
||||
// Answered by something the mesh holds, so there is no pair-wise secret between two machines.
|
||||
licence, _ := c.licence(n.From)
|
||||
@@ -189,6 +190,16 @@ func providerMoved(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool
|
||||
return known && (movedProvider(provider) || provider.AnsweredByHelperFor(n.Name))
|
||||
}
|
||||
|
||||
// answeredByHelper is whether a need's provider offers it answered by the provider helper (issue 792): keyed by the seat
|
||||
// mesh-provisioner for every consumer, handed or not (catalogue.Keyed, issue 774).
|
||||
func answeredByHelper(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
|
||||
if n.ByRecord || n.Module == "" {
|
||||
return false
|
||||
}
|
||||
provider, known := shelf[n.Module]
|
||||
return known && provider.AnsweredByHelperFor(n.Name)
|
||||
}
|
||||
|
||||
// movedProvider is whether a provider's credentials are the vault's: the module is marked moved, or it holds the
|
||||
// mesh-vault seat (issue 793).
|
||||
func movedProvider(m catalogue.Manifest) bool {
|
||||
|
||||
@@ -358,3 +358,31 @@ func TestALicenceKeyTheInputDoesNotCarryIsAStandInAndNamedForeseen(t *testing.T)
|
||||
t.Errorf("a key the mesh holds none of: %q %v", n.Sealed, got.Foreseen)
|
||||
}
|
||||
}
|
||||
|
||||
// A consumer not handed of a provision whose provider's offer says the helper answers it has that provision in a plan of
|
||||
// its own (issue 774: grafana's route): the composition marks the need answered by the helper, and the plan keys it.
|
||||
func TestANeedAnsweredByTheHelperIsInTheConsumersPlan(t *testing.T) {
|
||||
in := theStoreMesh(t)
|
||||
store := in.Catalogue["store"]
|
||||
store.Provides = []catalogue.Offer{{Name: "postgres-database", Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}}
|
||||
in.Catalogue["store"] = store
|
||||
got, err := Resolve(in, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := need(t, got.Plan, "board", "postgres-database"); !n.AnsweredByHelper {
|
||||
t.Fatalf("the need is not marked answered by the helper: %+v", n)
|
||||
}
|
||||
with := rendered(t, in, "laptop", Foreseeing).Rendering
|
||||
with.Hands, with.Provisioner = true, "mesh-provisioner-client"
|
||||
if with.Generators, err = newComposer(in).generators(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := got.Plan.Compose(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := c.Plans["board"].Provisions; len(p) != 1 || p[0].Provision != "postgres-database" || p[0].ProviderModule != "store" {
|
||||
t.Fatalf("board's plan is %+v", c.Plans["board"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user