The assigner's rule 8 waited for a key for every `requires`, while the plans named only the needs of handed modules: every new build of grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited for a key nobody would make, and grafana, not handed, had no plan, so no route key and no route on route-proxy. catalogue.Keyed is the rule (a need a module answers is keyed when its consumer is handed or the provider's helper answers it), catalogue.Awaited the same rule from manifests for the assigner, and a module not handed now has a plan for what a helper answers of it. Handed modules' plans are unchanged. Quick fix: drop the missing-key check from rule 8. Thorough fix (chosen): one rule in mesh-sdk all three parts use, since the kind (three parts deciding the same set apart) already drifted once and hid failures as waits.
306 lines
12 KiB
Go
306 lines
12 KiB
Go
package compose
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// LicenceProvision is what a module requires in order to be given a licence (the controller's licences.Provision).
|
|
const LicenceProvision = "model-access"
|
|
|
|
// Resolved is what one node should run, from what was assigned to it, with the settings of everything that resolved.
|
|
type Resolved struct {
|
|
Plan catalogue.Resolution
|
|
Settings catalogue.SettingsBy
|
|
// Foreseen are the needs composed with a stand-in for a credential the next send makes (or holds with no value given
|
|
// here), as "<module> on <node> needs <provision> from <provider>". Nothing was made.
|
|
Foreseen []string
|
|
// NotGiven are the words of each credential only a person gives that nobody has: the consumer still resolves without it
|
|
// (novox/hq ADR 0337 clause 4).
|
|
NotGiven []string
|
|
}
|
|
|
|
// Resolve works out everything a node should run, from what was assigned to it (the controller's planFor, plan.go): the
|
|
// resolution over the rest of the mesh, the credential of each thing the node takes from elsewhere, and the settings of
|
|
// every module that resolved. It makes and writes nothing: a credential not held is composed with a stand-in and named
|
|
// foreseen, one only a person gives and nobody has is left out and said, one the send would refuse is refused with the
|
|
// controller's words. A failure to compose the node's own modules is an error that satisfies IsUnresolvable, told apart
|
|
// from an unreadable Input (novox/hq 04-ISSUES/152).
|
|
func Resolve(in Input, node string) (Resolved, error) {
|
|
return newComposer(in).planFor(node)
|
|
}
|
|
|
|
// IsUnresolvable reports whether err is a node's own set failing to compose, rather than the mesh being unable to answer.
|
|
func IsUnresolvable(err error) bool { return unresolvable(err) }
|
|
|
|
// planFor is the controller's planFor.
|
|
func (c *composer) planFor(nodeName string) (Resolved, error) {
|
|
resolved, err := c.resolution(nodeName)
|
|
if err != nil {
|
|
return Resolved{}, err
|
|
}
|
|
node, err := c.in.node(nodeName)
|
|
if err != nil {
|
|
return Resolved{}, err
|
|
}
|
|
shelf := c.shelfFor(node)
|
|
var out Resolved
|
|
|
|
// The credential for each thing this node takes from elsewhere.
|
|
for i, n := range resolved.Needs {
|
|
// 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)
|
|
if licence.Keys == nil {
|
|
// The keys are not given: composed with a stand-in and named, not failed for a value only a send reads.
|
|
resolved.Needs[i].Sealed = catalogue.ForeseenSealed
|
|
out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From))
|
|
} else {
|
|
// Given, and none for this holder: the mesh holds none.
|
|
resolved.Needs[i].Sealed = licence.Keys[Holder{Node: nodeName, Module: n.For}]
|
|
}
|
|
// If this holder is the licence's manager, hand it the manager node's PUBLIC sealing key in its bound facts
|
|
// (novox/hq ADR 0050).
|
|
if pub := c.managerPublicKeyFor(licence, nodeName, n.For); pub != "" {
|
|
serves := map[string]any{}
|
|
for k, v := range resolved.Needs[i].Serves {
|
|
serves[k] = v
|
|
}
|
|
serves["manager_public_key"] = pub
|
|
resolved.Needs[i].Serves = serves
|
|
// The manager holder: its empty pre-adoption refresh token is a waiting state, not a missing consumer key.
|
|
resolved.Needs[i].Manager = true
|
|
}
|
|
continue
|
|
}
|
|
cred, held := c.credential(n, nodeName)
|
|
_, outside := issuedOutsideBy(shelf, n)
|
|
switch {
|
|
case cred.State == CredentialRefused:
|
|
// Said rather than skipped. A machine that resolves cleanly and receives no credential is one that will fail to
|
|
// authenticate at some later, less obvious moment.
|
|
return Resolved{}, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %s",
|
|
n.For, nodeName, n.Name, n.From, cred.Why)
|
|
case cred.State == CredentialNotGiven, outside && !(held && cred.State == CredentialHeld):
|
|
// Not given, and the rest of the consumer runs (ADR 0337 clause 4): no credential file, so the module can tell
|
|
// it was not given; nothing the mesh made stands in for it.
|
|
why := cred.Why
|
|
if why == "" {
|
|
why = "nobody gave it"
|
|
}
|
|
out.NotGiven = append(out.NotGiven, fmt.Sprintf("%s on %s holds no %s from %s: %s", n.For, nodeName, n.Name, n.From, why))
|
|
continue
|
|
case cred.State == CredentialHeld && cred.Sealed != "":
|
|
resolved.Needs[i].Sealed = cred.Sealed
|
|
default:
|
|
// Held with no value here, or none held: the next send makes it (or reads it), and a stand-in is composed.
|
|
resolved.Needs[i].Sealed = catalogue.ForeseenSealed
|
|
out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From))
|
|
}
|
|
}
|
|
|
|
// Settings for everything that resolved, including modules nobody assigned directly: a requirement pulled in by
|
|
// something else is still configurable.
|
|
settings := catalogue.SettingsBy{}
|
|
for _, m := range resolved.Modules {
|
|
layers := node.Settings[m.Module]
|
|
if len(layers) == 0 {
|
|
continue
|
|
}
|
|
settings[m.Module] = append([]catalogue.Layer(nil), layers...)
|
|
}
|
|
out.Plan, out.Settings = resolved, settings
|
|
return out, nil
|
|
}
|
|
|
|
// credential is how the pair credential of one need stands in the Input, and whether the Input names it at all.
|
|
func (c *composer) credential(n catalogue.Needed, consumer string) (Credential, bool) {
|
|
for _, cr := range c.in.Credentials {
|
|
if cr.Provision == n.Name && cr.Consumer == consumer && cr.ConsumerModule == n.For && cr.Provider == n.From && cr.Local == n.Local {
|
|
return cr, true
|
|
}
|
|
}
|
|
return Credential{State: CredentialWouldMake}, false
|
|
}
|
|
|
|
// licence is the licence of that name.
|
|
func (c *composer) licence(name string) (Licence, bool) {
|
|
for _, l := range c.in.Licences {
|
|
if l.Name == name {
|
|
return l, true
|
|
}
|
|
}
|
|
return Licence{}, false
|
|
}
|
|
|
|
// managerPublicKeyFor is the manager node's public sealing key, but only when (node, module) is the licence's manager holder
|
|
// and empty otherwise (plan.go managerPublicKeyFor): delivered to the manager module in its bound facts so it can re-seal a
|
|
// rotated refresh token to this node (novox/hq ADR 0050), and to no consumer.
|
|
func (c *composer) managerPublicKeyFor(l Licence, node, module string) string {
|
|
if l.Manager == (Holder{}) || node != l.Manager.Node || module != l.Manager.Module {
|
|
return ""
|
|
}
|
|
n, err := c.in.node(node)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return n.SealingKey
|
|
}
|
|
|
|
// issuedOutsideBy is whether a need's provision is offered by its provider with a credential issued outside the mesh
|
|
// (novox/hq ADR 0337), and that provider module: the one answering it when known, else any that offers it so.
|
|
func issuedOutsideBy(shelf map[string]catalogue.Manifest, n catalogue.Needed) (string, bool) {
|
|
if n.Module != "" {
|
|
if m, known := shelf[n.Module]; known {
|
|
return n.Module, m.IssuedOutsideOf(n.Name)
|
|
}
|
|
}
|
|
return catalogue.OffersIssuedOutside(shelf, n.Name)
|
|
}
|
|
|
|
// issuedToHolder is whether the vault keeps a need's credential for its holder alone (issues 749, 757 and 779).
|
|
func issuedToHolder(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
|
|
if n.ByRecord || n.Module == "" {
|
|
return false
|
|
}
|
|
provider, known := shelf[n.Module]
|
|
if !known || !movedProvider(provider) {
|
|
return false
|
|
}
|
|
if _, issued := provider.IssuedCredentialOf(n.Name); issued {
|
|
return true
|
|
}
|
|
return provider.CredentialFormOf(n.Name) == catalogue.CredentialFormPair
|
|
}
|
|
|
|
// providerMoved is whether a need is answered by a module marked moved (issue 788), or by one whose offer of the provision
|
|
// says the provider helper answers it (issue 792).
|
|
func providerMoved(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
|
|
if n.ByRecord || n.Module == "" {
|
|
return false
|
|
}
|
|
provider, known := shelf[n.Module]
|
|
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 {
|
|
return m.Moved || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == "mesh-vault" })
|
|
}
|
|
|
|
// resolution works out what one node should run, from what was assigned to it: the controller's planFor up to and
|
|
// including the resolution (plan.go planFor), and no further. A failure to compose the node's own modules is wrapped as
|
|
// notResolvable; every other failure is returned as it is.
|
|
func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
|
|
node, err := c.in.node(nodeName)
|
|
if err != nil {
|
|
return catalogue.Resolution{}, err
|
|
}
|
|
shelf := c.shelfFor(node)
|
|
world, _, left, err := c.theRestOfTheMesh(nodeName)
|
|
if err != nil {
|
|
return catalogue.Resolution{}, err
|
|
}
|
|
world.Pinned = node.Pins
|
|
// Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232).
|
|
world.Bound = node.Bound
|
|
|
|
onNetwork, err := c.whereEveryoneIs()
|
|
if err != nil {
|
|
return catalogue.Resolution{}, err
|
|
}
|
|
|
|
// What this mesh can answer with a record rather than a machine, and which record each of this node's modules was
|
|
// put on.
|
|
world.Licences, world.Using = c.licencesFor(nodeName)
|
|
|
|
resolved, err := catalogue.Resolve(shelf, node.Assigned,
|
|
catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities,
|
|
At: onNetwork[nodeName], PublicDomain: node.PublicDomain,
|
|
Account: node.Account, AccountHome: node.AccountHome,
|
|
AgentAccount: node.AgentAccount, AgentAccountHome: node.AgentAccountHome}, world)
|
|
if err != nil {
|
|
// What another node's set would have offered is missing when that set does not resolve here: said with the
|
|
// refusal, so "nothing provides" is read beside why (issue 1126, probe #532).
|
|
if len(left) > 0 {
|
|
var why []string
|
|
for _, l := range left {
|
|
why = append(why, l.String())
|
|
}
|
|
err = fmt.Errorf("%w\n\nand %d node(s) offer nothing here, as they could not be resolved: %s", err, len(left), strings.Join(why, "; "))
|
|
}
|
|
if len(c.unhostable) > 0 {
|
|
err = fmt.Errorf("%w\n\nand these modules offer nothing here, as their node cannot run them in this composition: %s",
|
|
err, strings.Join(c.unhostable, "; "))
|
|
}
|
|
// The node's own set does not compose: the only failure a mesh-wide gatherer may pass over.
|
|
return catalogue.Resolution{}, notResolvable{err}
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the
|
|
// build it runs put in place of the current one (plan.go keepRecorded). A copy: the Input's catalogue is untouched. Only the node's own planFor and
|
|
// resolution use it; the rest of the mesh and who is on the network are read with the current manifests, as the controller reads them
|
|
// before it keeps a recorded module.
|
|
func (c *composer) shelfFor(node Node) map[string]catalogue.Manifest {
|
|
if len(node.Kept) == 0 {
|
|
return c.in.Catalogue
|
|
}
|
|
shelf := make(map[string]catalogue.Manifest, len(c.in.Catalogue)+len(node.Kept))
|
|
for name, m := range c.in.Catalogue {
|
|
shelf[name] = m
|
|
}
|
|
for name, m := range node.Kept {
|
|
shelf[name] = m
|
|
}
|
|
return shelf
|
|
}
|
|
|
|
// licencesFor is what this node can be answered with by record, and what it was put on. A mesh with no licences at all
|
|
// is the ordinary case and not an error.
|
|
func (c *composer) licencesFor(node string) (map[string][]catalogue.Record, map[string]map[string]catalogue.Record) {
|
|
all := c.in.Licences
|
|
if len(all) == 0 {
|
|
return nil, nil
|
|
}
|
|
offered := map[string][]catalogue.Record{}
|
|
byName := map[string]catalogue.Record{}
|
|
for _, one := range all {
|
|
record := catalogue.Record{Name: one.Name, Serves: one.Serves}
|
|
offered[LicenceProvision] = append(offered[LicenceProvision], record)
|
|
byName[one.Name] = record
|
|
}
|
|
using := map[string]map[string]catalogue.Record{}
|
|
for _, one := range all {
|
|
for _, h := range one.Holders {
|
|
if h.Node != node {
|
|
continue
|
|
}
|
|
if using[h.Module] == nil {
|
|
using[h.Module] = map[string]catalogue.Record{}
|
|
}
|
|
using[h.Module][LicenceProvision] = byName[one.Name]
|
|
}
|
|
}
|
|
return offered, using
|
|
}
|