139 lines
5.2 KiB
Go
139 lines
5.2 KiB
Go
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Answer is a node's answer to its group's placement candidates (novox/hq ADR 0559 §4): s.Node composed in shadow with
|
|
// s.Placements laid over the trunk's placement (compose.CompositionsShadow, its own sealed values and boxes through r as
|
|
// for its own composition), compared with what it runs (delivery.CompareComposed). It applies nothing and writes
|
|
// nothing; head and revision are the candidate's, said back in the answer.
|
|
//
|
|
// fps gives the build each module is composed at: what the node runs, and for a module the candidate places that the
|
|
// node does not run, the build the trunk holds; a placed module with none is added, with no build of it known. What the
|
|
// composition stands in for is judged: a value of a module the group adds (on this node or another: its credential, own
|
|
// secrets and boxes are made only once it is placed, ADR 0549 §5) is not yet issued, which is no refusal; any other
|
|
// stand-in is a value missing for a module the node runs, a refusal naming it ("node" when it is the node's own).
|
|
func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string, running []delivery.Composed, head string, revision uint64) delivery.CandidateAnswer {
|
|
a := delivery.CandidateAnswer{Head: head, Revision: revision}
|
|
refuse := func(err error) delivery.CandidateAnswer {
|
|
a.Refused = err.Error()
|
|
a.Verdict = delivery.CandidateRefused
|
|
return a
|
|
}
|
|
trunk, err := r.Placements(ctx)
|
|
if err != nil {
|
|
return refuse(fmt.Errorf("frombus: placement: %w", err))
|
|
}
|
|
in, err := Input(ctx, r, mesh, s)
|
|
if err != nil {
|
|
return refuse(err)
|
|
}
|
|
asConsumer, asProvider, err := compose.CredentialsNeeded(in, s.Node)
|
|
if err != nil {
|
|
return refuse(err)
|
|
}
|
|
if len(asConsumer)+len(asProvider) > 0 {
|
|
if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil {
|
|
return refuse(fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err))
|
|
}
|
|
}
|
|
composed, skipped, stood, err := compose.CompositionsShadow(in, s.Node, fps)
|
|
if err != nil {
|
|
return refuse(err)
|
|
}
|
|
cmp := delivery.CompareComposed(composed, running)
|
|
a.Same, a.Differs, a.Removed = cmp.Same, cmp.Differs, cmp.Removed
|
|
|
|
// What the group adds, by node and module: placed by a candidate and not by the trunk on that node.
|
|
added := map[string]bool{}
|
|
for node, p := range s.Placements {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
was := map[string]bool{}
|
|
for _, m := range trunk[node].Modules {
|
|
was[m.Module] = true
|
|
}
|
|
for _, m := range p.Modules {
|
|
if !was[m.Module] {
|
|
added[node+"/"+m.Module] = true
|
|
}
|
|
}
|
|
}
|
|
unissued := map[string][]string{}
|
|
refusals := map[string][]string{}
|
|
for _, x := range stood {
|
|
node, module := standsInFor(x, s.Node)
|
|
switch {
|
|
case module != "" && added[node+"/"+module]:
|
|
if node == s.Node {
|
|
unissued[module] = append(unissued[module], x)
|
|
} // another node's addition: the module here that carries it differs, which the assigner judges
|
|
case module == "" || node != s.Node:
|
|
// The node's own, or a value this node holds for a module elsewhere (a grant it provides): said as the node's.
|
|
refusals[delivery.HaveNodeWord] = append(refusals[delivery.HaveNodeWord], x)
|
|
default:
|
|
refusals[module] = append(refusals[module], x)
|
|
}
|
|
}
|
|
addedHere := map[string]delivery.ModuleAdded{}
|
|
for _, m := range cmp.Added {
|
|
addedHere[m.Module] = m
|
|
}
|
|
for _, m := range skipped { // placed, and no build given to compose it at
|
|
addedHere[m] = delivery.ModuleAdded{Module: m, Unissued: []string{"no build of " + m + " is known to compose it at"}}
|
|
}
|
|
for m, xs := range unissued {
|
|
got := addedHere[m]
|
|
got.Module = m
|
|
got.Unissued = append(got.Unissued, xs...)
|
|
addedHere[m] = got
|
|
}
|
|
for _, m := range sortedKeys(addedHere) {
|
|
a.Added = append(a.Added, addedHere[m])
|
|
}
|
|
for _, m := range sortedKeys(refusals) {
|
|
if _, isAdded := addedHere[m]; isAdded {
|
|
continue // a module the candidate adds here: what it lacks is said under Added
|
|
}
|
|
a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: "stands in for " + strings.Join(refusals[m], ", ")})
|
|
}
|
|
sort.Slice(a.Refusals, func(i, j int) bool { return a.Refusals[i].Module < a.Refusals[j].Module })
|
|
a.Verdict = delivery.VerdictOf(a)
|
|
return a
|
|
}
|
|
|
|
var (
|
|
grantStandIn = regexp.MustCompile(`^the grant of \S+ for (\S+) on (\S+)$`)
|
|
needsStandIn = regexp.MustCompile(`^(\S+) on (\S+) needs `)
|
|
)
|
|
|
|
// standsInFor is the node and module a stand-in of compose's words is for: an own secret `module/name`, a port
|
|
// `port module:port`, a grant `the grant of <provision> for <module> on <node>`, a need `<module> on <node> needs ...`;
|
|
// the module empty when the words name none (the certificate, the authority).
|
|
func standsInFor(x, node string) (string, string) {
|
|
if m := grantStandIn.FindStringSubmatch(x); m != nil {
|
|
return m[2], m[1]
|
|
}
|
|
if m := needsStandIn.FindStringSubmatch(x); m != nil {
|
|
return m[2], m[1]
|
|
}
|
|
if rest, ok := strings.CutPrefix(x, "port "); ok {
|
|
if module, _, ok := strings.Cut(rest, ":"); ok {
|
|
return node, module
|
|
}
|
|
}
|
|
if module, _, ok := strings.Cut(x, "/"); ok && !strings.Contains(x, " ") {
|
|
return node, module
|
|
}
|
|
return node, ""
|
|
}
|