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. // // The node's own composition is compared only where the node runs one. 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) } // What the node runs, by module. A consumer's credential is asked only for a module it runs: the vault refuses the // whole ask for a consuming module not assigned to the node (mesh-vault's own), and a module the candidate adds is // assigned only after the merge (ADR 0549 §5), so its credential is left missing, a stand-in answered as not yet // issued under Added (review of L2b, round 1, finding 1). runs := map[string]bool{} for _, c := range running { if c.Module != "" { runs[c.Module] = true } } var consumed []compose.CredentialKey for _, k := range asConsumer { if runs[k.ConsumerModule] { consumed = append(consumed, k) } } asConsumer = consumed 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) } // The node's own composition is judged only where the node runs one: one that runs none applies it only once a want // names it (the node agent's own composer hands it on that want alone), so a candidate does not add it. runsOwn := false for _, c := range running { runsOwn = runsOwn || c.Module == "" } if !runsOwn { for key, c := range composed { if c.Module == "" { delete(composed, key) } } } 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 node == s.Node && runs[module]: // A value missing for a module the node runs is a refusal, whatever the trunk places (the seed's trunk is // empty, and every module counts as the group's). refusals[module] = append(refusals[module], x) 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 for on `, a need ` on 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, "" }