336 lines
12 KiB
Go
336 lines
12 KiB
Go
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())
|
|
}
|
|
out.Said = c.saidBeside(out.Said)
|
|
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]
|
|
// A module kept at the build the machine runs is carried at that build (plan.go carriedOn).
|
|
if kept, ok := node.KeptBuilds[m.Module]; ok && kept != "" {
|
|
out[m.Module] = kept
|
|
}
|
|
}
|
|
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
|
|
}
|