Compose a node's declaration over an Input and validate it with the node-engine's own validator: Compose and ComposeIn (issue 1298). Ported from mesh-control 6c2a8d62 plan.go declarationWith, carriedOn, wireBuilds, sendable.go Body and adoptionFor, facts.go composedAndValidated, in the Foreseeing mode: it allocates, makes, keeps and writes nothing. Named Compose, not Node, because the Input already has a type Node. A pull request's check refuses what Declared.Refusals() returns beside the validator's problems.
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/validate"
|
||||
)
|
||||
|
||||
// Declared is a declaration as a machine is sent it (the controller's sendable, sendable.go): its resources and, for an adopted
|
||||
// node, its mode and which modules were taken on it (novox/hq ADR 0100), with what the composition says beside it and never puts on
|
||||
// the wire.
|
||||
//
|
||||
// **Body is the only place the envelope is marshalled**, so the digest the mesh compares and the body validated are one.
|
||||
type Declared struct {
|
||||
Resources []map[string]any
|
||||
// Sequence, Epoch and Generation are what the Input gave the node: zero is not sent at all.
|
||||
Sequence int64
|
||||
Epoch uint64
|
||||
Generation int64
|
||||
// ReadsModules says the machine's node-engine reads a module per resource and a build per module (novox/hq issue 1145): only
|
||||
// then are they sent. ModuleOf is the composer's owner of each resource id and BuildIDs the build each module carries.
|
||||
ReadsModules bool
|
||||
ModuleOf map[string]string
|
||||
BuildIDs map[string]string
|
||||
// Adoption is nil for a converged node, and then the body is byte for byte what it was before adoption existed.
|
||||
Adoption *Adoption
|
||||
// Filled is the path of every file this declaration hands to the provisioner, and Provisioner the account its client runs as
|
||||
// (novox/hq ADR 0349): on the wire as `filled_files` and `provisioner_account`, both absent when nothing is handed.
|
||||
Filled []string
|
||||
Provisioner string
|
||||
// LeftOut is every module of the machine's set left out because a stored setting cannot compose with its definition (novox/hq
|
||||
// ADR 0163, rule 6), sorted; LeftOutWhy is why each was.
|
||||
LeftOut []string
|
||||
LeftOutWhy map[string]string
|
||||
|
||||
// Not on the wire:
|
||||
Received map[string]map[string][]catalogue.Contribution
|
||||
Mesh []string
|
||||
BusUsers string
|
||||
// Modules are the modules this declaration names, sorted.
|
||||
Modules []string
|
||||
// LocalNetwork is every port admitted from the local network and what it resolved to (novox/hq ADR 0340).
|
||||
LocalNetwork []catalogue.LocalNetworkPort
|
||||
Withheld []catalogue.Overflow
|
||||
Unbound []catalogue.Unbound
|
||||
Unplaced []string
|
||||
HandedOnly []string
|
||||
// RequiredBy is, for each handed module only pulled in by another, the module that requires it (issue 765).
|
||||
RequiredBy map[string]string
|
||||
// Foreseen is every own secret composed with a stand-in, as `module/name`; StoodIn is everything the composition stood in for
|
||||
// (an own secret, a grant's sealed copy, the certificate, the bus's user list); ForeseenPorts the ports the mesh may assign that
|
||||
// the Input holds none for. A declaration that has any is never sent: what a send would make, named, never invented.
|
||||
Foreseen []string
|
||||
StoodIn []string
|
||||
ForeseenPorts []string
|
||||
// CoreBoxesWanted are the core credentials whose box the send must ask the vault for and keep.
|
||||
CoreBoxesWanted []CoreBoxWanted
|
||||
// Passes are the seats the mesh would pass on, for the caller to record, and Said the lines found while composing.
|
||||
Passes Passes
|
||||
Said []string
|
||||
}
|
||||
|
||||
// Adoption is what an adopted node is told about its mode. Taken is every module taken on it that it runs; Untaken is, for every
|
||||
// module it runs that is not taken, the ids of every one of that module's resources: what the host keeps as found until the module
|
||||
// is taken (ADR 0103).
|
||||
type Adoption struct {
|
||||
Taken []string `json:"taken"`
|
||||
Untaken map[string][]string `json:"untaken,omitempty"`
|
||||
}
|
||||
|
||||
// Body is the declaration's bytes, as sent and as digested.
|
||||
func (d Declared) Body() ([]byte, error) {
|
||||
envelope := map[string]any{"declaration": 1, "resources": d.Resources}
|
||||
if d.ReadsModules {
|
||||
// The module of each resource the composer owns, and the build of each module (novox/hq issue 1145): to a machine that
|
||||
// said it reads them, and no other.
|
||||
named := make([]map[string]any, len(d.Resources))
|
||||
for n, r := range d.Resources {
|
||||
module, owned := d.ModuleOf[fmt.Sprint(r["id"])]
|
||||
if !owned {
|
||||
named[n] = r
|
||||
continue
|
||||
}
|
||||
with := make(map[string]any, len(r)+1)
|
||||
for k, v := range r {
|
||||
with[k] = v
|
||||
}
|
||||
with["module"] = module
|
||||
named[n] = with
|
||||
}
|
||||
envelope["resources"] = named
|
||||
if len(d.BuildIDs) > 0 {
|
||||
envelope["builds"] = d.BuildIDs
|
||||
}
|
||||
}
|
||||
if d.Adoption != nil {
|
||||
envelope["adoption"] = d.Adoption
|
||||
}
|
||||
if d.Sequence > 0 {
|
||||
envelope["sequence"] = d.Sequence
|
||||
}
|
||||
if d.Epoch > 0 {
|
||||
envelope["epoch"] = d.Epoch
|
||||
}
|
||||
if len(d.LeftOut) > 0 {
|
||||
envelope["left_out"] = d.LeftOut
|
||||
}
|
||||
if d.Generation > 0 {
|
||||
envelope["generation"] = d.Generation
|
||||
}
|
||||
if len(d.Filled) > 0 {
|
||||
envelope["filled_files"] = d.Filled
|
||||
envelope["provisioner_account"] = d.Provisioner
|
||||
}
|
||||
// An empty declaration is deliberate: the node owns nothing the mesh put there (novox/hq issue 127), and the host refuses an
|
||||
// empty body unless it is told the emptiness is meant.
|
||||
if len(d.Resources) == 0 {
|
||||
envelope["owns_nothing"] = true
|
||||
}
|
||||
return json.Marshal(envelope)
|
||||
}
|
||||
|
||||
// Refusals are the findings a check refuses a change for, beside the validator's problems: a consumer left out of the grants
|
||||
// because its identity overflows the provision's bound, and a credential bound to another consumer (novox/hq ADR 0446 clause 1).
|
||||
// Each names the consumer and the provision.
|
||||
func (d Declared) Refusals() []string {
|
||||
var out []string
|
||||
for _, o := range d.Withheld {
|
||||
out = append(out, o.String())
|
||||
}
|
||||
for _, u := range d.Unbound {
|
||||
out = append(out, u.String())
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Compose composes one node's declaration (the issue calls it Node; the Input already has a type of that name) over an Input -- as a push would, without the sending (the controller's
|
||||
// composedAndValidated in the Foreseeing mode) -- and runs the node-engine's own validator over its body. An error is that the
|
||||
// node did not compose (a set that does not compose satisfies IsUnresolvable); problems are what the validator refuses, in the
|
||||
// node-engine's words. It allocates, makes, keeps and writes nothing, and names what a send would do as foreseen.
|
||||
func Compose(in Input, node string) (Declared, []string, error) {
|
||||
return ComposeIn(in, node, Foreseeing)
|
||||
}
|
||||
|
||||
// ComposeIn is Compose in a chosen mode.
|
||||
func ComposeIn(in Input, node string, mode Mode) (Declared, []string, error) {
|
||||
c := newComposer(in)
|
||||
d, err := c.declaration(node, mode)
|
||||
if err != nil {
|
||||
return Declared{}, nil, err
|
||||
}
|
||||
body, err := d.Body()
|
||||
if err != nil {
|
||||
return Declared{}, nil, err
|
||||
}
|
||||
return d, validate.Declaration(body), nil
|
||||
}
|
||||
|
||||
// declaration is the controller's declarationWith over the plain Input.
|
||||
func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) {
|
||||
resolved, err := c.planFor(nodeName)
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
gens, err := c.generators()
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
rendered, err := c.render(nodeName, mode)
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
with := rendered.Rendering
|
||||
with.Generators = gens
|
||||
plan := resolved.Plan
|
||||
composed, err := plan.Compose(with)
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
// Which ports the filter admits from the local network, and from which networks (novox/hq ADR 0340 clause 8): the same rules the
|
||||
// composition rendered, never sent.
|
||||
rules, err := plan.Rules(with)
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
node, err := c.in.node(nodeName)
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
out := Declared{Resources: composed.Resources, Adoption: adoptionFor(plan, node, composed),
|
||||
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
|
||||
LeftOut: sortedKeysOf(composed.LeftOut), LeftOutWhy: composed.LeftOut,
|
||||
Withheld: rendered.Withheld, Unbound: rendered.Unbound,
|
||||
Foreseen: composed.Foreseen, StoodIn: rendered.Foreseen, ForeseenPorts: rendered.ForeseenPorts,
|
||||
Unplaced: composed.Unplaced, HandedOnly: composed.HandedOnly, Modules: namedModules(plan, composed.LeftOut),
|
||||
Filled: composed.Filled, LocalNetwork: catalogue.LocalNetworkPorts(rules, with.LocalNetworks),
|
||||
CoreBoxesWanted: rendered.CoreBoxesWanted, Said: rendered.Said,
|
||||
Sequence: node.Sequence, Epoch: node.Epoch, Generation: node.Generation}
|
||||
if len(composed.Filled) > 0 {
|
||||
out.Provisioner = with.Provisioner
|
||||
}
|
||||
// A handed module only pulled in by another keeps what requires it, for its value of its own in the assignments mirror (issue 765).
|
||||
for module := range composed.Plans {
|
||||
if by := plan.RequiredBy(module); by != "" {
|
||||
if out.RequiredBy == nil {
|
||||
out.RequiredBy = map[string]string{}
|
||||
}
|
||||
out.RequiredBy[module] = by
|
||||
}
|
||||
}
|
||||
// And the module of each resource and the build of each module, to a machine whose node-engine said it reads them (novox/hq issue
|
||||
// 1145) and to no other.
|
||||
if node.ReadsModules {
|
||||
carried := c.carriedOn(node, plan, composed.LeftOut)
|
||||
out.ReadsModules, out.ModuleOf, out.BuildIDs = true, composed.Owner, c.wireBuilds(carried, node.Runs)
|
||||
}
|
||||
_, passes, left, err := c.theRestOfTheMesh("")
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
out.Passes = passes
|
||||
for _, l := range left {
|
||||
out.Said = append(out.Said, l.String())
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// carriedOn is the build of each module a declaration for the node carries (plan.go carriedOn, in a question that sends nothing): the
|
||||
// module's current build for each module in it, and for a module left out of it the build it was last sent, since the machine keeps
|
||||
// that one -- or nothing, when that is not known.
|
||||
func (c *composer) carriedOn(node Node, plan catalogue.Resolution, leftOut map[string]string) map[string]string {
|
||||
before := node.SentBuilds
|
||||
if !node.SentKnown {
|
||||
before = nil
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, m := range plan.Modules {
|
||||
if _, left := leftOut[m.Module]; left {
|
||||
if was, kept := before[m.Module]; kept {
|
||||
out[m.Module] = was
|
||||
}
|
||||
continue
|
||||
}
|
||||
out[m.Module] = c.in.CurrentBuilds[m.Module]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// wireBuilds is the build of each carried module that goes on the wire (novox/hq issue 1145): the build its assignment names when
|
||||
// that assignment is of the commit carried, else the build made from that commit; a module with neither is not named.
|
||||
func (c *composer) wireBuilds(carried map[string]string, runs map[string]Run) map[string]string {
|
||||
out := map[string]string{}
|
||||
for module, commit := range carried {
|
||||
if commit == "" {
|
||||
continue
|
||||
}
|
||||
if ref, ok := runs[module]; ok && ref.Commit == commit && ref.Build != "" {
|
||||
out[module] = ref.Build
|
||||
continue
|
||||
}
|
||||
if build := c.in.BuildIDs[module][commit]; build != "" {
|
||||
out[module] = build
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// adoptionFor is the envelope computed from what was taken and who owns each resource: nil for a converged node. Every module the
|
||||
// node runs that is not taken is untaken -- including one pulled in by another rather than assigned.
|
||||
func adoptionFor(plan catalogue.Resolution, node Node, composed catalogue.Composed) *Adoption {
|
||||
if !node.Adopted {
|
||||
return nil
|
||||
}
|
||||
isTaken := map[string]bool{}
|
||||
for _, m := range node.Taken {
|
||||
isTaken[m] = true
|
||||
}
|
||||
out := &Adoption{Taken: []string{}}
|
||||
runs := map[string]bool{}
|
||||
for _, m := range plan.Modules {
|
||||
runs[m.Module] = true
|
||||
if isTaken[m.Module] {
|
||||
out.Taken = append(out.Taken, m.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out.Taken)
|
||||
for _, r := range composed.Resources {
|
||||
id, _ := r["id"].(string)
|
||||
module, owned := composed.Owner[id]
|
||||
// Every kind, not only files and containers (novox/hq ADR 0103). What the mesh declares of its own is never held.
|
||||
if !owned || !runs[module] || isTaken[module] || strings.HasPrefix(id, catalogue.AdoptionPrefix) {
|
||||
continue
|
||||
}
|
||||
if out.Untaken == nil {
|
||||
out.Untaken = map[string][]string{}
|
||||
}
|
||||
out.Untaken[module] = append(out.Untaken[module], id)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sortedKeysOf is a map's keys, sorted: so what a declaration says it left out does not move for a reordering nobody made.
|
||||
func sortedKeysOf(m map[string]string) []string {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// namedModules are the modules a declaration names: its machine's set, less what was left out of it.
|
||||
func namedModules(plan catalogue.Resolution, leftOut map[string]string) []string {
|
||||
out := make([]string, 0, len(plan.Modules))
|
||||
for _, m := range plan.Modules {
|
||||
if _, left := leftOut[m.Module]; !left {
|
||||
out = append(out, m.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user