Resolve one node over an Input, its credentials as stand-ins and its settings (issue 1362). Ported from mesh-control 6c2a8d62 planFor. Nothing is made: a pair credential is how it stands (held, to make, only a person's to give, refused), never its value; keepRecorded is left to the caller, who puts the kept manifest in the Catalogue. The bus credential nobody issued and a given own secret under an old key are refused where the controller refuses them, in the rendering (issue 1364).
This commit is contained in:
@@ -50,6 +50,11 @@ type Input struct {
|
||||
CarriedPeers []CarriedPeer
|
||||
// Joining is the nodes that hold a live enrolment token (inv.NodesWithALiveToken).
|
||||
Joining []string
|
||||
|
||||
// Credentials is how each pair credential a node takes from elsewhere stands, never its value (what inv.SecretFor,
|
||||
// SharedSecretFor and GivenPairSecret would answer): held, to be made by the next send, only a person's to give, or
|
||||
// refused. A need with no entry here is one the next send would make.
|
||||
Credentials []Credential
|
||||
}
|
||||
|
||||
// Node is one node of the mesh and what the composition reads about it.
|
||||
@@ -76,6 +81,9 @@ type Node struct {
|
||||
Settings map[string][]catalogue.Layer
|
||||
// Ports is the machine port each module's software port was given on it (inv.PortsFor), by module then port.
|
||||
Ports map[string]map[int]int
|
||||
// SealingKey is the public key its secrets are sealed to (inv.SealingKeyOf); empty when it has none. A public key, so
|
||||
// nothing secret.
|
||||
SealingKey string
|
||||
}
|
||||
|
||||
// Place is where a node is, as its placement says (inv.Overlay).
|
||||
@@ -138,6 +146,11 @@ type Licence struct {
|
||||
Name string
|
||||
Serves map[string]any
|
||||
Holders []Holder
|
||||
// Keys is, by holder, the licence's key sealed to that holder's machine (held.KeyFor), empty where the mesh holds none:
|
||||
// the plaintext was discarded when it was supplied. Optional; the composition needs no value of it.
|
||||
Keys map[Holder]string
|
||||
// Manager is the holder of a refreshable-grant licence that manages it (held.ManagerOf); the zero Holder when none.
|
||||
Manager Holder
|
||||
}
|
||||
|
||||
// Holder is a module on a node that uses a licence.
|
||||
@@ -165,3 +178,36 @@ func (in Input) sorted() []Node {
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// CredentialState is how one pair credential stands.
|
||||
type CredentialState string
|
||||
|
||||
const (
|
||||
// CredentialHeld is a credential held for both ends as they stand now. Sealed, when given, is the consumer's copy; a held
|
||||
// credential with no value is composed with a stand-in, named foreseen.
|
||||
CredentialHeld CredentialState = "held"
|
||||
// CredentialWouldMake is a credential none is held for: the next send makes it, and the composition uses a stand-in,
|
||||
// named foreseen. It is also what a need with no entry is.
|
||||
CredentialWouldMake CredentialState = "would-make"
|
||||
// CredentialNotGiven is a credential only a person gives (novox/hq ADR 0337 clause 4) that nobody has given, or that was given
|
||||
// under a key one end no longer has: the consumer still resolves, without it. Why is the controller's words for it.
|
||||
CredentialNotGiven CredentialState = "not-given"
|
||||
// CredentialRefused is a credential the send would refuse to make, for the reason Why gives in the controller's words: a
|
||||
// value a person gave sealed to a key the machine no longer has, a machine with no sealing key.
|
||||
CredentialRefused CredentialState = "refused"
|
||||
)
|
||||
|
||||
// Credential is the state of one pair credential, by the ends it joins: the provision, the consumer's node and module, the
|
||||
// provider's node, and the local name where a module keeps several (inv.Secret's key).
|
||||
type Credential struct {
|
||||
Provision string
|
||||
Consumer string
|
||||
ConsumerModule string
|
||||
Provider string
|
||||
Local string
|
||||
State CredentialState
|
||||
// Sealed is the consumer's copy as it is sealed to the consumer's key; optional, and only the controller's adapter passes it.
|
||||
Sealed string
|
||||
// Why is the words of a CredentialNotGiven or CredentialRefused.
|
||||
Why string
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (c *composer) whoResolves(requirement string) (map[string]bool, map[string]
|
||||
// Why a node could not be resolved, kept rather than raised: one broken node must not stop the rest being described.
|
||||
refused := map[string]string{}
|
||||
for _, n := range c.in.sorted() {
|
||||
plan, err := c.resolution(n.Name)
|
||||
resolved, err := c.planFor(n.Name)
|
||||
switch {
|
||||
case unresolvable(err):
|
||||
refused[n.Name] = err.Error()
|
||||
@@ -133,7 +133,7 @@ func (c *composer) whoResolves(requirement string) (map[string]bool, map[string]
|
||||
// Not a node that does not resolve: a question that went unanswered.
|
||||
return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err)
|
||||
}
|
||||
for _, m := range plan.Modules {
|
||||
for _, m := range resolved.Plan.Modules {
|
||||
for _, offered := range m.Offers() {
|
||||
if offered == requirement {
|
||||
on[n.Name] = true
|
||||
|
||||
+181
-2
@@ -1,16 +1,195 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"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.in.Catalogue
|
||||
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)
|
||||
if n.ByRecord {
|
||||
// Answered by something the mesh holds, so there is no pair-wise secret between two machines.
|
||||
licence, _ := c.licence(n.From)
|
||||
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))
|
||||
}
|
||||
|
||||
// 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. What it leaves to the issue after is the credentials of
|
||||
// what the node takes from elsewhere and its settings.
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user