Vendor mesh-sdk's go/compose and drop the node-engine's validator from vendor/ (issues 1381, 1382)
go mod vendor writes go/compose (copied on the forge from mesh-sdk 14fd5cb68fd1, equal to its blobs) now that the adapter imports it, and no longer writes github.com/novox/mesh-host/validate and internal/declaration, which nothing imports since the validator comes from mesh-sdk (issue 1381; the swap commits left those five files behind). vendor/modules.txt follows with the adapter.
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Mode says what a composition does about what a send would make. Both modes read what the Input holds and make nothing; they
|
||||
// differ as the controller's Reading and Foreseeing modes do.
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
// Reading composes what is held and leaves out an own secret the Input holds none for: that machine cannot be running it.
|
||||
Reading Mode = iota
|
||||
// Foreseeing composes an own secret the next send would make with a stand-in and names it foreseen, while one the send would
|
||||
// refuse (a bus credential nobody issued, a given secret under an old key) is refused here, in the same words.
|
||||
Foreseeing
|
||||
)
|
||||
|
||||
// ForeseenCertificate and ForeseenBusUsers stand for what the Input does not carry, in a composition asked ahead of the send.
|
||||
// Never sent.
|
||||
const (
|
||||
ForeseenCertificate = "foreseen: issued by the next send"
|
||||
ForeseenBusUsers = "foreseen: composed from the bus records by the next send"
|
||||
)
|
||||
|
||||
// grantsFor is every credential this node must create, because something elsewhere uses it (plan.go grantsFor): the mirror of
|
||||
// what a consumer is given. A consumer whose identity overflows the provision's bound is left out of the grants and returned
|
||||
// beside them (novox/hq ADR 0225); one whose resolution binds it to another provider is withdrawn and returned beside them
|
||||
// (issue 274). Every other is granted.
|
||||
func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, []string, error) {
|
||||
issued, err := c.rowsFor(node)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
onNetwork, err := c.whereEveryoneIs()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
out := make([]catalogue.Grant, 0, len(issued))
|
||||
var withheld []catalogue.Overflow
|
||||
var unbound []catalogue.Unbound
|
||||
var foreseen []string
|
||||
for _, s := range issued {
|
||||
resolved, err := c.planFor(s.Consumer)
|
||||
switch {
|
||||
case unresolvable(err):
|
||||
// Their set does not resolve. Skipped rather than fatal: this node is not the place to report another machine's problem.
|
||||
continue
|
||||
case err != nil:
|
||||
// The mesh could not be asked what they wanted, which is not the same as their wanting nothing.
|
||||
return nil, nil, nil, nil, fmt.Errorf("what %s asked of %s cannot be read: %w", s.Consumer, s.Provision, err)
|
||||
}
|
||||
plan := resolved.Plan
|
||||
values, asks, err := plan.ContributionsFrom(s.Provision, s.ConsumerModule, resolved.Settings)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// A port in there is the consumer's software port until this: the assignment that moved it is that machine's.
|
||||
consumer, err := c.in.node(s.Consumer)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
values = catalogue.AtPublishedPort(values, s.ConsumerModule, consumer.Ports[s.ConsumerModule])
|
||||
from := s.ConsumerModule
|
||||
if !asks {
|
||||
// That module no longer wants this: left empty, which is how a login is withdrawn rather than kept working for ever.
|
||||
from = ""
|
||||
}
|
||||
if bound := plan.BindsFrom(s.Provision, s.ConsumerModule, s.Local); from != "" && !slices.Contains(bound, node) {
|
||||
// It still asks, and not of this node: withdrawn exactly as a credential nobody asks for, and said.
|
||||
from = ""
|
||||
unbound = append(unbound, catalogue.Unbound{Provision: s.Provision, Provider: node,
|
||||
Consumer: s.Consumer, Module: s.ConsumerModule, Local: s.Local, BoundTo: bound})
|
||||
}
|
||||
// The consumer's identity slug, from its own manifest, carried on the grant so the provider derives the same login the
|
||||
// consumer does (novox/hq ADR 0049).
|
||||
slug := ""
|
||||
for _, mm := range plan.Modules {
|
||||
if mm.Module == s.ConsumerModule {
|
||||
slug = mm.Slug
|
||||
break
|
||||
}
|
||||
}
|
||||
if from != "" {
|
||||
bound := boundOfGrant(plan, s, node)
|
||||
source := catalogue.IdentitySource(slug, s.ConsumerModule)
|
||||
if catalogue.CheckIdentityWithin(s.Consumer, source, bound) != nil {
|
||||
withheld = append(withheld, catalogue.Overflow{Provision: s.Provision, Provider: node,
|
||||
Consumer: s.Consumer, Module: s.ConsumerModule,
|
||||
Identity: catalogue.ConsumerIdentity(s.Consumer, source), Bound: bound})
|
||||
continue
|
||||
}
|
||||
}
|
||||
sealed := s.ForProvider
|
||||
if sealed == "" {
|
||||
sealed = catalogue.ForeseenSealed
|
||||
foreseen = append(foreseen, fmt.Sprintf("the grant of %s for %s on %s", s.Provision, s.ConsumerModule, s.Consumer))
|
||||
}
|
||||
out = append(out, catalogue.Grant{
|
||||
Provision: s.Provision, Consumer: s.Consumer, At: onNetwork[s.Consumer],
|
||||
From: from, Values: values, Slug: slug, Sealed: sealed, Local: s.Local})
|
||||
}
|
||||
return out, withheld, unbound, foreseen, nil
|
||||
}
|
||||
|
||||
// rowsFor is every pair credential this node's declaration must carry a grant for, in the order the controller reads them
|
||||
// (SecretsFrom: by provision, consumer, consumer module and local name): the ones the Input says were made, and the ones the same send
|
||||
// would make before it composes this node -- every need of a node that resolves (this one too), whose provider is this node, that is not
|
||||
// answered by a record and not a credential only a person gives and nobody gave. Those have no sealed copy (ForProvider empty), so the
|
||||
// grant is composed with a stand-in and named foreseen (plan.go: planFor makes the secret in every mode before grantsFor reads it).
|
||||
func (c *composer) rowsFor(node string) ([]Issued, error) {
|
||||
var rows []Issued
|
||||
for _, row := range c.in.Issued {
|
||||
if row.Provider == node {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
for _, other := range c.in.sorted() {
|
||||
resolved, err := c.planFor(other.Name)
|
||||
switch {
|
||||
case unresolvable(err):
|
||||
continue
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("what %s asked of %s cannot be read: %w", other.Name, node, err)
|
||||
}
|
||||
shelf := c.shelfFor(other)
|
||||
for _, n := range resolved.Plan.Needs {
|
||||
if n.From != node || n.ByRecord {
|
||||
continue
|
||||
}
|
||||
cred, held := c.credential(n, other.Name)
|
||||
if _, outside := issuedOutsideBy(shelf, n); cred.State == CredentialNotGiven || (outside && !(held && cred.State == CredentialHeld)) {
|
||||
continue
|
||||
}
|
||||
if slices.ContainsFunc(rows, func(r Issued) bool {
|
||||
return r.Provision == n.Name && r.Consumer == other.Name && r.ConsumerModule == n.For && r.Local == n.Local
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, Issued{Provision: n.Name, Consumer: other.Name, ConsumerModule: n.For, Local: n.Local, Provider: node})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
a, b := rows[i], rows[j]
|
||||
switch {
|
||||
case a.Provision != b.Provision:
|
||||
return a.Provision < b.Provision
|
||||
case a.Consumer != b.Consumer:
|
||||
return a.Consumer < b.Consumer
|
||||
case a.ConsumerModule != b.ConsumerModule:
|
||||
return a.ConsumerModule < b.ConsumerModule
|
||||
}
|
||||
return a.Local < b.Local
|
||||
})
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// boundOfGrant is the identity bound the consumer's own resolution states for the requirement this grant answers. A requirement
|
||||
// not found there is held to the tightest bound the mesh knows rather than to none.
|
||||
func boundOfGrant(consumer catalogue.Resolution, s Issued, provider string) catalogue.IdentityBound {
|
||||
for _, n := range consumer.Needs {
|
||||
if n.Name == s.Provision && n.For == s.ConsumerModule && n.From == provider {
|
||||
return n.Identity
|
||||
}
|
||||
}
|
||||
return catalogue.DefaultIdentityBound
|
||||
}
|
||||
|
||||
// ownSecretRow is the row for one own secret of a module on a node.
|
||||
func (c *composer) ownSecretRow(node, module, name string) (OwnSecretRow, bool) {
|
||||
for _, r := range c.in.OwnSecrets {
|
||||
if r.Node == node && r.Module == module && r.Name == name {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
return OwnSecretRow{}, false
|
||||
}
|
||||
|
||||
// givenMember is one own secret a person gave on a machine and whether it is sealed to the key the machine holds now.
|
||||
type givenMember struct {
|
||||
Name string
|
||||
Sealed string
|
||||
Current bool
|
||||
}
|
||||
|
||||
// givenMembers is every member of a module's secret family given on a machine, by name (inv.GivenMembers): the mesh never
|
||||
// makes a member, so only a value a person gave is one; a machine with no sealing key holds none.
|
||||
func (c *composer) givenMembers(node Node, module, family string) []givenMember {
|
||||
if node.SealingKey == "" {
|
||||
return nil
|
||||
}
|
||||
prefix := catalogue.FamilyPrefix(family)
|
||||
var out []givenMember
|
||||
for _, r := range c.in.OwnSecrets {
|
||||
if r.Node == node.Name && r.Module == module && r.Origin == "accepted" && strings.HasPrefix(r.Name, prefix) {
|
||||
out = append(out, givenMember{Name: r.Name, Sealed: r.Sealed, Current: r.SealedTo == node.SealingKey})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// givenOwnSecret is an own secret issued outside the mesh as a person gave it on a machine (inv.GivenOwnSecret): not given when
|
||||
// the value held was made by the mesh or the machine has no sealing key.
|
||||
func (c *composer) givenOwnSecret(node Node, module, name string) (givenMember, bool) {
|
||||
if node.SealingKey == "" {
|
||||
return givenMember{}, false
|
||||
}
|
||||
r, ok := c.ownSecretRow(node.Name, module, name)
|
||||
if !ok || r.Origin != "accepted" {
|
||||
return givenMember{}, false
|
||||
}
|
||||
return givenMember{Name: name, Sealed: r.Sealed, Current: r.SealedTo == node.SealingKey}, true
|
||||
}
|
||||
|
||||
// moduleSecretIfIssued is the read half of the controller's SecretForModule (inv.ModuleSecretIfIssued): the secret when one is
|
||||
// held and sealed to the key the machine holds now, and otherwise not held. A key it was sealed to that the machine no longer
|
||||
// has is reported as absent rather than as an error.
|
||||
func (c *composer) moduleSecretIfIssued(node Node, module, name string) (sealed string, held bool) {
|
||||
if node.SealingKey == "" {
|
||||
return "", false
|
||||
}
|
||||
r, ok := c.ownSecretRow(node.Name, module, name)
|
||||
if !ok || r.SealedTo != node.SealingKey {
|
||||
return "", false
|
||||
}
|
||||
return r.Sealed, true
|
||||
}
|
||||
|
||||
// wouldMakeSecretForModule says what the send would do about an own secret without doing it: nil when it would make one (or one
|
||||
// is held), and otherwise the very refusal it would meet (inv.WouldMakeSecretForModule).
|
||||
func (c *composer) wouldMakeSecretForModule(node Node, module, name string) error {
|
||||
if node.SealingKey == "" {
|
||||
return fmt.Errorf("%s needs a secret and %s has no sealing key, so nothing can be sealed to it", module, node.Name)
|
||||
}
|
||||
r, ok := c.ownSecretRow(node.Name, module, name)
|
||||
switch {
|
||||
case !ok:
|
||||
return nil
|
||||
case r.SealedTo != node.SealingKey && r.Origin == "accepted":
|
||||
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
||||
"since generated a new sealing key. The mesh cannot make another; issue it again", module, node.Name, name, node.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// busCredentialIssued refuses an own secret called `broker` whose bus account nobody issued (novox/hq issue 203): the broker
|
||||
// credential is never invented, an account on the bus minted by `module issue`.
|
||||
func (c *composer) busCredentialIssued(node, module, name string) error {
|
||||
var user string
|
||||
switch name {
|
||||
case "broker":
|
||||
user = node + "." + module
|
||||
case ProvisionerSecret:
|
||||
user = "provisioner." + node
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if !slices.Contains(c.in.BusMinted, user) {
|
||||
return fmt.Errorf(
|
||||
"%s on %s has no bus credential: nothing was issued for %s, and a push "+
|
||||
"would seal a placeholder its process cannot read (novox/hq issue 203). "+
|
||||
"`module issue %s --node %s`, then push again",
|
||||
module, node, user, module, node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// givenUnderAnOldKey is the refusal for a value a person gave sealed to a key the machine no longer holds.
|
||||
func givenUnderAnOldKey(module, node, name string) error {
|
||||
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
||||
"since generated a new sealing key. The mesh cannot make another; give it again", module, node, name, node)
|
||||
}
|
||||
|
||||
// ownSecretsFor is each module's own secrets as this node composes them (plan.go renderingFor): the needed ones sealed to the
|
||||
// node, and, in the Foreseeing mode, the ones the next send would make named foreseen. A module handed to the provisioner has its
|
||||
// secrets from the vault through the client, so none of them is composed here.
|
||||
func (c *composer) ownSecretsFor(node Node, plan catalogue.Resolution, handing catalogue.Rendering, mode Mode) (
|
||||
needed map[string]map[string]string, foreseen map[string]map[string]bool, err error) {
|
||||
needed = map[string]map[string]string{}
|
||||
foreseen = map[string]map[string]bool{}
|
||||
mark := func(module, name string) {
|
||||
if foreseen[module] == nil {
|
||||
foreseen[module] = map[string]bool{}
|
||||
}
|
||||
foreseen[module][name] = true
|
||||
}
|
||||
for _, m := range plan.Modules {
|
||||
if handing.Handing(m) {
|
||||
continue
|
||||
}
|
||||
// A secret family is never made (novox/hq ADR 0283): each member a person gave on this machine is placed, and one not
|
||||
// given is nothing -- the module says it waits for it.
|
||||
for _, family := range m.OwnSecrets.Families() {
|
||||
for _, g := range c.givenMembers(node, m.Module, family) {
|
||||
if _, fam, ok := m.OwnSecrets.Lookup(g.Name); !ok || fam != family {
|
||||
continue // a longer family's member, or a name no longer of this family
|
||||
}
|
||||
if !g.Current {
|
||||
continue
|
||||
}
|
||||
if needed[m.Module] == nil {
|
||||
needed[m.Module] = map[string]string{}
|
||||
}
|
||||
needed[m.Module][g.Name] = g.Sealed
|
||||
if g.Sealed == "" {
|
||||
// Given, and its value is not in the Input: placed with a stand-in, named.
|
||||
needed[m.Module][g.Name] = catalogue.ForeseenSealed
|
||||
mark(m.Module, g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
names := make([]string, 0)
|
||||
for name := range m.OwnSecrets.Plain() {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
own := m.OwnSecrets.Plain()[name]
|
||||
// An own secret issued outside the mesh is never made (novox/hq issue 401, ADR 0228; ADR 0283 for a family): only a
|
||||
// value a person gave is placed. One not given is nothing, and a value the mesh made for it before is not one either.
|
||||
if own.IssuedBy == catalogue.IssuedOutside {
|
||||
g, given := c.givenOwnSecret(node, m.Module, name)
|
||||
if !given || !g.Current {
|
||||
continue
|
||||
}
|
||||
if g.Sealed == "" {
|
||||
mark(m.Module, name)
|
||||
continue
|
||||
}
|
||||
if needed[m.Module] == nil {
|
||||
needed[m.Module] = map[string]string{}
|
||||
}
|
||||
needed[m.Module][name] = g.Sealed
|
||||
continue
|
||||
}
|
||||
sealed, held := c.moduleSecretIfIssued(node, m.Module, name)
|
||||
if !held && mode == Foreseeing {
|
||||
// Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in,
|
||||
// named; refused by it -- refused here, the same words (novox/hq issue 275).
|
||||
if err := c.busCredentialIssued(node.Name, m.Module, name); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := c.wouldMakeSecretForModule(node, m.Module, name); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mark(m.Module, name)
|
||||
continue
|
||||
}
|
||||
if !held {
|
||||
// Never issued, so this machine cannot be running it: left out rather than invented.
|
||||
continue
|
||||
}
|
||||
if sealed == "" {
|
||||
// Held, and its value is not in the Input: composed with a stand-in, named.
|
||||
mark(m.Module, name)
|
||||
continue
|
||||
}
|
||||
if needed[m.Module] == nil {
|
||||
needed[m.Module] = map[string]string{}
|
||||
}
|
||||
needed[m.Module][name] = sealed
|
||||
}
|
||||
}
|
||||
return needed, foreseen, nil
|
||||
}
|
||||
|
||||
// CoreBoxWanted is a core credential whose box the Input does not hold for a machine: the next send asks the vault for it and
|
||||
// keeps the answer. It is returned for the caller to ask and keep; this package asks and keeps nothing.
|
||||
type CoreBoxWanted struct {
|
||||
Node string
|
||||
Module string
|
||||
Name string
|
||||
}
|
||||
|
||||
// coreModulesOf is every module of the plan on the core's list whose own secrets this machine's declaration carries, with their
|
||||
// plain own secret names, sorted: one handed to the provisioner has its secrets from the vault through the client, and one issued
|
||||
// outside the mesh is never the vault's to seal here.
|
||||
func coreModulesOf(modules []catalogue.Manifest, handed func(catalogue.Manifest) bool) map[string][]string {
|
||||
out := map[string][]string{}
|
||||
for _, m := range modules {
|
||||
if !catalogue.CarriesVaultSealed(m.Module) || (handed != nil && handed(m)) {
|
||||
continue
|
||||
}
|
||||
var names []string
|
||||
for name, own := range m.OwnSecrets.Plain() {
|
||||
if own.IssuedBy != catalogue.IssuedOutside {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
if len(names) > 0 {
|
||||
sort.Strings(names)
|
||||
out[m.Module] = names
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sealingKeyValid says a machine's sealing key, recorded raw in base64, is a raw X25519 key.
|
||||
func sealingKeyValid(raw string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(raw))
|
||||
if err != nil || len(b) != 32 {
|
||||
return errors.New("the machine's sealing key is not a raw X25519 key")
|
||||
}
|
||||
_, err = ecdh.X25519().NewPublicKey(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// coreBoxesFor decides each core credential of a machine in a composition that sends nothing and asks no vault (core_box.go
|
||||
// coreBoxesOf, in the modes that are not a send): the kept box while it is sealed to the key the machine holds now, and otherwise a
|
||||
// stand-in that the next send replaces by asking the vault -- returned in wanted for the caller to ask and keep. A machine with no
|
||||
// sealing key, or one whose key is not a key, leaves its module out, said.
|
||||
func (c *composer) coreBoxesFor(node Node, modules []catalogue.Manifest, handing catalogue.Rendering) (
|
||||
sealed map[string]map[string]json.RawMessage, notPlaced map[string]string, wanted []CoreBoxWanted) {
|
||||
core := coreModulesOf(modules, func(m catalogue.Manifest) bool { return handing.Handing(m) })
|
||||
sealed, notPlaced = map[string]map[string]json.RawMessage{}, map[string]string{}
|
||||
mods := make([]string, 0, len(core))
|
||||
for m := range core {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
sort.Strings(mods)
|
||||
for _, module := range mods {
|
||||
if node.SealingKey == "" {
|
||||
notPlaced[module] = fmt.Sprintf("%s has no sealing key, so the vault can seal %s's credentials to "+
|
||||
"nothing there (novox/hq ADR 0349 §16)", node.Name, module)
|
||||
continue
|
||||
}
|
||||
if err := sealingKeyValid(node.SealingKey); err != nil {
|
||||
notPlaced[module] = fmt.Sprintf("%s's credentials cannot be asked of the vault: %v", module, err)
|
||||
continue
|
||||
}
|
||||
boxes := map[string]json.RawMessage{}
|
||||
for _, name := range core[module] {
|
||||
var kept *CoreBox
|
||||
for i := range c.in.KeptCoreBoxes {
|
||||
k := &c.in.KeptCoreBoxes[i]
|
||||
if k.Node == node.Name && k.Module == module && k.Name == name {
|
||||
kept = k
|
||||
}
|
||||
}
|
||||
if kept != nil && kept.SealedTo == node.SealingKey && len(kept.Box) > 0 {
|
||||
boxes[name] = kept.Box
|
||||
continue
|
||||
}
|
||||
boxes[name] = catalogue.ForeseenVaultBox
|
||||
wanted = append(wanted, CoreBoxWanted{Node: node.Name, Module: module, Name: name})
|
||||
}
|
||||
sealed[module] = boxes
|
||||
}
|
||||
return sealed, notPlaced, wanted
|
||||
}
|
||||
|
||||
// certificateFor is what the mesh certifies about one machine's internal name (plan.go certificateFor): refused for a machine that
|
||||
// never told the mesh what key it serves with, and for one not on the private network, which has no name to be certified for.
|
||||
// The certificate and the authority are public values the Input may carry; without them stand-ins are composed, and named.
|
||||
func (c *composer) certificateFor(node Node) (certificate, authority string, foreseen bool, err error) {
|
||||
if node.ServingKey == "" {
|
||||
return "", "", false, fmt.Errorf(
|
||||
"%s wants a certificate and has never told the mesh what key it serves with; it "+
|
||||
"joins again to report one", node.Name)
|
||||
}
|
||||
where, err := c.whereEveryoneIs()
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
if where[node.Name] == "" {
|
||||
return "", "", false, fmt.Errorf(
|
||||
"%s wants a certificate and is not on the private network, so it has no name inside "+
|
||||
"the mesh to be certified for", node.Name)
|
||||
}
|
||||
certificate, authority = node.Certificate, c.in.Authority
|
||||
if certificate == "" || authority == "" {
|
||||
return ForeseenCertificate, ForeseenCertificate, true, nil
|
||||
}
|
||||
return certificate, authority, false, nil
|
||||
}
|
||||
|
||||
// holdsTheBus says whether a node's set includes the module that asks for the bus's user list.
|
||||
func holdsTheBus(modules []catalogue.Manifest) bool {
|
||||
for _, m := range modules {
|
||||
if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// Package compose is the controller's composition of a machine, as a function over a plain input (novox/hq ADR 0453
|
||||
// clause 3, ADR 0446 clause 4): what the rest of the mesh offers and holds, the private network's generator, one
|
||||
// node's resolution and, in the issues after, its rendering and its declaration.
|
||||
//
|
||||
// **It never allocates, makes, writes, logs or raises.** The controller composes with a store behind it; this
|
||||
// package is its Reading and Foreseeing modes over data. Everything the controller reads from its store is a field of
|
||||
// Input, and everything a send would make or record (a port, a credential, a seat passed on, a node left out) is
|
||||
// returned for the caller to act on, never done. Input is never changed by a call. No field holds a secret value
|
||||
// the composition needs: sealed values are optional inputs, and without them the composition uses stand-ins named
|
||||
// foreseen. The logic is the controller's, ported from mesh-controller cmd/mesh-controller at commit
|
||||
// 6c2a8d62100cb7d491f8eb9a623e1eeea5ec0de1 (plan.go, network.go, holdings.go, seat_pass.go), in the same order, with the
|
||||
// same refusals in the same words, so that a comparison node for node is a fair one.
|
||||
//
|
||||
// It imports the standard library and mesh-sdk's own packages only.
|
||||
// It reads no environment variable: the range the mesh allocates from and the suffix of its internal names are fields of Input.
|
||||
package compose
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Input is everything the composition reads. Each field is what one read of the controller's inventory answers, named
|
||||
// beside it; the controller's adapter fills it from its stores and the assigner from the mesh's facts and buckets
|
||||
// (ADR 0446 clause 2).
|
||||
type Input struct {
|
||||
// Catalogue is the shelf: every module's manifest by name (inv.Catalogue), as it is to be composed. A recorded
|
||||
// module kept at the build a machine runs (keepRecorded) is that node's Kept, applied to its own resolution only (never to the rest of the mesh).
|
||||
Catalogue map[string]catalogue.Manifest
|
||||
// Nodes is every node the mesh has (inv.Nodes and inv.Overlays together, one row per node).
|
||||
Nodes []Node
|
||||
// Holdings is every seat whose holder is on record (inv.Holdings).
|
||||
Holdings []catalogue.Held
|
||||
// Licences is every licence the mesh holds, with the machines that use it (open.Licences); none for a mesh with no
|
||||
// licence.
|
||||
Licences []Licence
|
||||
|
||||
// OverlayCIDR is the range the mesh allocates from when no tunnel was adopted, as genesis was told it (the
|
||||
// controller's OverlayCIDRVar); empty means DefaultOverlayCIDR. The package reads no environment.
|
||||
OverlayCIDR string
|
||||
// InternalSuffix is what the mesh's internal names end in (the controller's MESH_INTERNAL_SUFFIX), with or without a leading
|
||||
// dot; empty means overlay.DefaultSuffix. A fact of the whole mesh, so it is in the Input and never read from the environment.
|
||||
InternalSuffix string
|
||||
// Tunnels is, by node name, the found tunnel whose key is the node's overlay key (inv.Tunnels).
|
||||
Tunnels map[string]FoundTunnel
|
||||
// Adopted is the hub's found tunnel and the hub's name, whether or not the hub took it (inv.AdoptedTunnel); nil
|
||||
// when no hub has one.
|
||||
Adopted *AdoptedTunnel
|
||||
// CarriedPeers is every peer of the adopted tunnel (inv.CarriedPeers).
|
||||
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
|
||||
|
||||
// Built is every repository the mesh has built (the keys of inv.Held): what tells a reference recorded with an address
|
||||
// from an image a module runs straight from a public registry.
|
||||
Built []string
|
||||
// OperatorExport is every operator-sealed secret in the mesh, for a module that keeps them (inv.OperatorExport): sealed
|
||||
// values the composition carries without opening. Nil when the mesh has no operator key.
|
||||
OperatorExport *catalogue.KeptExport
|
||||
|
||||
// Issued is the credentials made for the consumers of each provider (inv.SecretsFrom): one row for each pair credential
|
||||
// that exists, by the provider node it is for. The grants a provider must create are read from these, so a credential not
|
||||
// made yet has no grant until the send that makes it.
|
||||
Issued []Issued
|
||||
// OwnSecrets is the own secrets held on each machine (the module_secret rows): who made each, and the key it is sealed to.
|
||||
// Their sealed values are optional.
|
||||
OwnSecrets []OwnSecretRow
|
||||
// KeptCoreBoxes is the vault's box last kept for each core credential on each machine (inv.KeptCoreBoxes).
|
||||
KeptCoreBoxes []CoreBox
|
||||
// BusUsers is the bus's user list as the controller composes it from the bus records and the hashes it holds (composeBusUsers),
|
||||
// for the machine that runs the bus: names and hashes, never passwords. Empty when the Input does not carry it, and then
|
||||
// the composition uses a stand-in, named foreseen. BusMissing are the users left out of it for want of a credential.
|
||||
BusUsers string
|
||||
BusMissing []string
|
||||
// BusMinted are the bus users the mesh has minted a password hash for (inv.BusUserHash): `<node>.<module>` for a module and
|
||||
// `provisioner.<node>` for a machine's provisioner client.
|
||||
BusMinted []string
|
||||
// BusMemberships is, by node, the membership sealed to it for the bus the mesh is moving to (inv.BusMemberships).
|
||||
BusMemberships map[string]string
|
||||
// Authority is the mesh's own certificate (identity.EstablishAuthority), public. Optional.
|
||||
Authority string
|
||||
|
||||
// CurrentBuilds is, by module, the commit of the build the mesh holds for it (inv.CurrentBuilds, the commit it was built from);
|
||||
// BuildIDs the build made from a commit of a module (inv.BuildOf), by module then commit. Read only to name the builds a
|
||||
// declaration carries to a node-engine that reads them.
|
||||
CurrentBuilds map[string]string
|
||||
BuildIDs map[string]map[string]string
|
||||
}
|
||||
|
||||
// Issued is one pair credential made for a consumer of a provider (inv.Secret): ForProvider is its copy sealed to the
|
||||
// provider, optional.
|
||||
type Issued struct {
|
||||
Provision string
|
||||
Consumer string
|
||||
ConsumerModule string
|
||||
Local string
|
||||
Provider string
|
||||
ForProvider string
|
||||
}
|
||||
|
||||
// OwnSecretRow is one own secret held on a machine: Origin is `made` (the mesh generated it) or `accepted` (a person gave it),
|
||||
// SealedTo the sealing key it is sealed to, and Sealed its value sealed to that key, optional.
|
||||
type OwnSecretRow struct {
|
||||
Node string
|
||||
Module string
|
||||
Name string
|
||||
Origin string
|
||||
SealedTo string
|
||||
Sealed string
|
||||
}
|
||||
|
||||
// CoreBox is the vault's box for one core credential of one machine, as last kept (inv.CoreBox).
|
||||
type CoreBox struct {
|
||||
Node string
|
||||
Module string
|
||||
Name string
|
||||
Generation int
|
||||
SealedTo string
|
||||
Box json.RawMessage
|
||||
}
|
||||
|
||||
// Node is one node of the mesh and what the composition reads about it.
|
||||
type Node struct {
|
||||
Name string
|
||||
// Place is where it is on the private network (inv.Overlays).
|
||||
Place Place
|
||||
// Assigned is the modules assigned to it, in the order the controller reads them (inv.Assigned).
|
||||
Assigned []string
|
||||
// Capabilities are the ones present on it (inv.ProfileOf).
|
||||
Capabilities map[string]bool
|
||||
// PublicDomain is the domain it composes its routed names under (inv.PublicDomainOf).
|
||||
PublicDomain string
|
||||
// Account, AccountHome, AgentAccount and AgentAccountHome are its operator and agent logins (inv.NodeByName).
|
||||
Account, AccountHome, AgentAccount, AgentAccountHome string
|
||||
// Adopted is whether it is adopted rather than converged.
|
||||
Adopted bool
|
||||
// Pins is the provider each provision was pinned to for it (inv.PinsFor).
|
||||
Pins map[string]catalogue.Chosen
|
||||
// Bound is where each of its consumers of a provision that keeps data was last sent (inv.BindingsFor), by consumer
|
||||
// module and provision.
|
||||
Bound map[string]map[string]catalogue.Chosen
|
||||
// Settings is its settings layers by module (inv.SettingsFor); a module with none is absent.
|
||||
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
|
||||
// Taken is the modules taken on it, for an adopted node (inv.Taken).
|
||||
Taken []string
|
||||
// OutwardLinks are the links it reported as facing outside it (inv.OutwardLinksOf).
|
||||
OutwardLinks []string
|
||||
// Networks are the networks it reported being attached to, as link and network (inv.MachineOf); NetworksSaid is whether
|
||||
// its node-engine said any at all, which is not the same as saying none.
|
||||
Networks []catalogue.AttachedNetwork
|
||||
NetworksSaid bool
|
||||
// ServingKey is the key the machine serves with, which a certificate is issued for (identity.ServingKeyOf); empty when it has
|
||||
// never reported one. Certificate is the certificate the mesh issued for its internal name, public and optional.
|
||||
ServingKey string
|
||||
Certificate string
|
||||
// Sequence, Epoch and Generation are what the declaration is sent under (inv.Sequence, SentEpoch, SentGeneration): the order
|
||||
// against every other send to the node, the lease epoch and the assignment generation it was last sent. Zero is not sent.
|
||||
Sequence int64
|
||||
Epoch uint64
|
||||
Generation int64
|
||||
// ReadsModules says its node-engine reads a module per resource and a build per module (inv.ReadsModules, issue 1145); only
|
||||
// then are they sent. SentBuilds is the build of each module it was last sent, by module to commit, and SentKnown whether
|
||||
// that is known at all (inv.SentBuilds); Runs is what each of its assignments runs (inv.AssignmentRuns), by module.
|
||||
ReadsModules bool
|
||||
SentBuilds map[string]string
|
||||
SentKnown bool
|
||||
Runs map[string]Run
|
||||
// Kept is, by module, the manifest of a recorded or core module kept at the build this machine runs (plan.go keepRecorded:
|
||||
// the caller reads it with recordedKept and ManifestAt), and KeptBuilds the commit of that build. They apply to this node's
|
||||
// own resolution only, never to another node's, and a declaration carries the kept commit for the module.
|
||||
Kept map[string]catalogue.Manifest
|
||||
KeptBuilds map[string]string
|
||||
// Contract is the contract of its node-engine's newest statement (inv.HealthOf); Stated is whether it ever stated one.
|
||||
Contract int
|
||||
Stated bool
|
||||
}
|
||||
|
||||
// Place is where a node is, as its placement says (inv.Overlay).
|
||||
type Place struct {
|
||||
Key string
|
||||
Endpoint string
|
||||
Site string
|
||||
Hub bool
|
||||
Address string
|
||||
}
|
||||
|
||||
// Reachable says whether the node can be dialled.
|
||||
func (p Place) Reachable() bool { return strings.TrimSpace(p.Endpoint) != "" }
|
||||
|
||||
// Tunnel is a tunnel found on a node (inv.Tunnel).
|
||||
type Tunnel struct {
|
||||
Interface string
|
||||
Unit string
|
||||
Config string
|
||||
Port int
|
||||
MTU int
|
||||
// Address is the interface's own address with its prefix length, Range the network that prefix names.
|
||||
Address string
|
||||
Range string
|
||||
PublicKey string
|
||||
Peers []TunnelPeer
|
||||
}
|
||||
|
||||
// TunnelPeer is one peer of a found tunnel.
|
||||
type TunnelPeer struct {
|
||||
PublicKey string
|
||||
Address string
|
||||
}
|
||||
|
||||
// FoundTunnel is a node's tunnel and whether the node is adopted (inv.FoundTunnel).
|
||||
type FoundTunnel struct {
|
||||
Tunnel
|
||||
NodeAdopted bool
|
||||
}
|
||||
|
||||
// AdoptedTunnel is the hub's tunnel, the hub's name, and whether the hub's overlay key is the tunnel's, which is
|
||||
// whether the private network took the tunnel over.
|
||||
type AdoptedTunnel struct {
|
||||
Tunnel Tunnel
|
||||
Hub string
|
||||
Adopted bool
|
||||
}
|
||||
|
||||
// CarriedPeer is a peer of the adopted tunnel (inv.CarriedPeer).
|
||||
type CarriedPeer struct {
|
||||
PublicKey string
|
||||
Address string
|
||||
EnrolledAs string
|
||||
Named string
|
||||
}
|
||||
|
||||
// Licence is one licence the mesh holds: its name, what a consumer may know of it, and the machines and modules
|
||||
// that use it.
|
||||
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: nil means not given, and the composition uses a stand-in named
|
||||
// foreseen; given (even empty), a holder with no entry is one the mesh holds no key for.
|
||||
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.
|
||||
type Holder struct {
|
||||
Node string
|
||||
Module string
|
||||
}
|
||||
|
||||
// ErrNoSuchNode is what the controller's inventory answers for a node it does not have.
|
||||
var ErrNoSuchNode = errors.New("no node of that name")
|
||||
|
||||
// node is the node of that name, or the refusal the controller's inventory gives for one it does not have.
|
||||
func (in Input) node(name string) (Node, error) {
|
||||
for _, n := range in.Nodes {
|
||||
if n.Name == name {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return Node{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
|
||||
}
|
||||
|
||||
// sorted is the nodes by name, the order the controller reads them in.
|
||||
func (in Input) sorted() []Node {
|
||||
out := append([]Node(nil), in.Nodes...)
|
||||
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
|
||||
}
|
||||
|
||||
// Run is the build an assignment names: the commit it is of and the build made from it (chain.Ref).
|
||||
type Run struct {
|
||||
Commit string
|
||||
Build string
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
)
|
||||
|
||||
// The private network: who is on it, where, and what they are called. Ported from mesh-controller
|
||||
// cmd/mesh-controller/network.go at 6c2a8d62.
|
||||
|
||||
// DefaultOverlayCIDR is the range the mesh allocates from when nothing says another.
|
||||
const DefaultOverlayCIDR = "10.42.0.0/16"
|
||||
|
||||
// composer is one composition over an Input: it reads Input, changes nothing in it, and keeps what the controller
|
||||
// said on its standard error as lines to return.
|
||||
type composer struct {
|
||||
in Input
|
||||
said []string
|
||||
seen map[string]bool
|
||||
}
|
||||
|
||||
func newComposer(in Input) *composer { return &composer{in: in, seen: map[string]bool{}} }
|
||||
|
||||
// say keeps a line the controller would have written to its log, once.
|
||||
func (c *composer) say(format string, args ...any) {
|
||||
line := fmt.Sprintf(format, args...)
|
||||
if c.seen[line] {
|
||||
return
|
||||
}
|
||||
c.seen[line] = true
|
||||
c.said = append(c.said, line)
|
||||
}
|
||||
|
||||
// suffix is what the mesh's internal names end in: the Input's, and overlay.DefaultSuffix when it gives none. The package never calls
|
||||
// overlay.Suffix or overlay.InternalName, which read the environment, so the assigner and the controller compose the same names
|
||||
// from the same Input.
|
||||
func (c *composer) suffix() string {
|
||||
if v := strings.TrimPrefix(strings.TrimSpace(c.in.InternalSuffix), "."); v != "" {
|
||||
return v
|
||||
}
|
||||
return overlay.DefaultSuffix
|
||||
}
|
||||
|
||||
// internalName is a node's name inside the mesh.
|
||||
func (c *composer) internalName(node string) string { return node + "." + c.suffix() }
|
||||
|
||||
// saidBeside is the lines already said and then every line the composer kept, once each, in the order they were found.
|
||||
func (c *composer) saidBeside(lines []string) []string {
|
||||
for _, l := range c.said {
|
||||
if !slices.Contains(lines, l) {
|
||||
lines = append(lines, l)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// overlayRange is the range the mesh allocates node addresses from: the adopted tunnel's first (novox/hq ADR 0105),
|
||||
// then the range genesis was told, then the default.
|
||||
func (c *composer) overlayRange() string {
|
||||
if a := c.in.Adopted; a != nil && a.Adopted {
|
||||
return a.Tunnel.Range
|
||||
}
|
||||
if v := strings.TrimSpace(c.in.OverlayCIDR); v != "" {
|
||||
return v
|
||||
}
|
||||
return DefaultOverlayCIDR
|
||||
}
|
||||
|
||||
// network builds the private network over the machines that resolved the module for it. A machine is on it because it
|
||||
// was given the module, or because it is joining with a key and an address (novox/hq ADR 0169).
|
||||
func (c *composer) network(on map[string]bool, refused map[string]string) (*overlay.Generator, error) {
|
||||
tunnels := c.in.Tunnels
|
||||
carried := c.in.CarriedPeers
|
||||
isJoining := map[string]bool{}
|
||||
for _, name := range c.in.Joining {
|
||||
isJoining[name] = true
|
||||
}
|
||||
var nodes []overlay.Node
|
||||
for _, node := range c.in.sorted() {
|
||||
p := node.Place
|
||||
if !on[node.Name] && !(isJoining[node.Name] && p.Key != "" && p.Address != "") {
|
||||
continue
|
||||
}
|
||||
n := overlay.Node{
|
||||
Name: node.Name, Key: p.Key, Endpoint: p.Endpoint,
|
||||
Site: p.Site, Hub: p.Hub, Address: p.Address,
|
||||
}
|
||||
if t, takes := tunnels[node.Name]; takes && t.NodeAdopted {
|
||||
// Only an adopted node is told to take the found unit over. A hub whose placement disagrees with the tunnel is
|
||||
// refused, not composed.
|
||||
if wrong := disagrees(p, t.Tunnel); wrong != "" {
|
||||
return nil, fmt.Errorf("%s takes over the tunnel on %s and its placement disagrees with it: %s. "+
|
||||
"Re-place it — `overlay place %s --hub --endpoint <host>:%d …` — and push again; "+
|
||||
"nothing was composed", node.Name, t.Interface, wrong, node.Name, t.Port)
|
||||
}
|
||||
n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU}
|
||||
}
|
||||
if p.Hub {
|
||||
for _, cp := range carried {
|
||||
n.Carried = append(n.Carried, overlay.Carried{Key: cp.PublicKey, Address: cp.Address})
|
||||
}
|
||||
}
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
// Nobody was given it: an empty network is a legitimate mesh, not a broken one.
|
||||
return overlay.Empty(), nil
|
||||
}
|
||||
g, err := overlay.From(nodes, c.overlayRange(), "")
|
||||
if err != nil && len(refused) > 0 {
|
||||
// The network is missing something, and some machines could not be resolved at all: almost always the same fact.
|
||||
var who []string
|
||||
for name, why := range refused {
|
||||
who = append(who, fmt.Sprintf(" %s: %s", name, why))
|
||||
}
|
||||
sort.Strings(who)
|
||||
return nil, fmt.Errorf("%w\n\nand %d node(s) could not be resolved at all, which is "+
|
||||
"probably why:\n%s", err, len(refused), strings.Join(who, "\n"))
|
||||
}
|
||||
return g, err
|
||||
}
|
||||
|
||||
// Graph is the whole mesh's network, for showing it: the nodes on it and each one's peers.
|
||||
func Graph(in Input) ([]overlay.Node, overlay.Graph, error) {
|
||||
c := newComposer(in)
|
||||
on, refused, err := c.whoResolves(overlay.Requirement)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
g, err := c.network(on, refused)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return g.Nodes(), g.Graph(), nil
|
||||
}
|
||||
|
||||
// whoResolves is the machines whose resolution answers a requirement, and why the others did not: by what a module
|
||||
// provides, not by its name, resolved rather than read from the assignments.
|
||||
func (c *composer) whoResolves(requirement string) (map[string]bool, map[string]string, error) {
|
||||
on := map[string]bool{}
|
||||
// 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() {
|
||||
resolved, err := c.planFor(n.Name)
|
||||
switch {
|
||||
case unresolvable(err):
|
||||
refused[n.Name] = err.Error()
|
||||
continue
|
||||
case err != nil:
|
||||
// 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 resolved.Plan.Modules {
|
||||
for _, offered := range m.Offers() {
|
||||
if offered == requirement {
|
||||
on[n.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return on, refused, nil
|
||||
}
|
||||
|
||||
// Generators is what every node's declaration is composed with, over the whole mesh: the private network's generator
|
||||
// (one generator now; the names and a resolver's zone file are facts a module asks for in its manifest).
|
||||
func Generators(in Input) (map[string]catalogue.Generator, error) {
|
||||
c := newComposer(in)
|
||||
return c.generators()
|
||||
}
|
||||
|
||||
func (c *composer) generators() (map[string]catalogue.Generator, error) {
|
||||
on, refused, err := c.whoResolves(overlay.Addressing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
net, err := c.network(on, refused)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]catalogue.Generator{overlay.Name: net}, nil
|
||||
}
|
||||
|
||||
// disagrees says how a node's placement differs from the tunnel it takes over — its address not the tunnel's, its
|
||||
// endpoint not on the tunnel's port — or nothing when both agree.
|
||||
func disagrees(p Place, t Tunnel) string {
|
||||
var wrong []string
|
||||
want := t.Address
|
||||
if i := strings.Index(want, "/"); i >= 0 {
|
||||
want = want[:i]
|
||||
}
|
||||
if p.Address != want {
|
||||
wrong = append(wrong, fmt.Sprintf("its address is %s and the tunnel's is %s", orNothing(p.Address), want))
|
||||
}
|
||||
if p.Reachable() && portOfEndpoint(p.Endpoint) != strconv.Itoa(t.Port) {
|
||||
wrong = append(wrong, fmt.Sprintf("its endpoint %s is not on the tunnel's port %d", p.Endpoint, t.Port))
|
||||
}
|
||||
return strings.Join(wrong, "; ")
|
||||
}
|
||||
|
||||
func orNothing(s string) string {
|
||||
if s == "" {
|
||||
return "unset"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// portOfEndpoint is the port in host:port, or empty.
|
||||
func portOfEndpoint(endpoint string) string {
|
||||
if i := strings.LastIndex(endpoint, ":"); i >= 0 {
|
||||
return endpoint[i+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// whereEveryoneIs is each machine's name on the private network, for the ones on it. Resolved without consulting the
|
||||
// rest of the mesh (it is called from that path), and refused without the catalogue.
|
||||
func (c *composer) whereEveryoneIs() (map[string]string, error) {
|
||||
if c.in.Catalogue == nil {
|
||||
return nil, errors.New("asked where everyone is without the catalogue, which cannot be answered")
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, p := range c.onTheNetwork() {
|
||||
out[p.Name] = c.internalName(p.Name)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// onTheNetwork is every placed machine that resolves the private network: has an address AND runs what puts it there
|
||||
// (novox/hq issue 079). A machine that does not resolve is not counted, and said.
|
||||
func (c *composer) onTheNetwork() []Node {
|
||||
var out []Node
|
||||
for _, n := range c.in.sorted() {
|
||||
if n.Place.Address == "" {
|
||||
continue
|
||||
}
|
||||
if len(n.Assigned) == 0 {
|
||||
continue
|
||||
}
|
||||
got, err := catalogue.Resolve(c.in.Catalogue, n.Assigned,
|
||||
catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities},
|
||||
catalogue.World{Unchecked: true, Holdings: c.in.Holdings})
|
||||
if err != nil {
|
||||
c.say("%s is not counted as on the network: it does not resolve: %v", n.Name, err)
|
||||
continue
|
||||
}
|
||||
for _, m := range got.Modules {
|
||||
for _, offered := range m.Offers() {
|
||||
if offered == overlay.Requirement {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Rendered is everything a node's declaration is composed with (plan.go renderingFor), over an Input: the place half (Places)
|
||||
// and the credential half. Nothing was allocated, made, kept or written; what a send would do is returned for the caller to do.
|
||||
type Rendered struct {
|
||||
Rendering catalogue.Rendering
|
||||
// ForeseenPorts are the ports the mesh may assign that the Input holds none for ("<module>:<port>").
|
||||
ForeseenPorts []string
|
||||
// Foreseen are the stand-ins the composition used for what the Input does not hold: an own secret the next send makes
|
||||
// ("<module>/<name>"), a grant's sealed copy, the certificate and the bus's user list.
|
||||
Foreseen []string
|
||||
// CoreBoxesWanted are the core credentials whose box the send must ask the vault for and keep.
|
||||
CoreBoxesWanted []CoreBoxWanted
|
||||
// Withheld are the consumers the grants leave out because their identity overflows the provision's bound (novox/hq ADR 0225),
|
||||
// and Unbound those whose credential from this node is on record and whose own resolution binds them elsewhere (issue
|
||||
// 274). The controller says them; a check refuses a change that adds one (ADR 0446 clause 1).
|
||||
Withheld []catalogue.Overflow
|
||||
Unbound []catalogue.Unbound
|
||||
// Said are the lines the controller says while composing (the bus users left out for want of a credential).
|
||||
Said []string
|
||||
}
|
||||
|
||||
// Render composes the rendering of a node: the grants it must create, where it puts its ports, its own secrets, the vault's boxes,
|
||||
// its certificate, the names of the mesh, the bus's user list and its membership. The refusals are the controller's, in its words;
|
||||
// where more than one fault stands, the first reported may differ from the controller's, which interleaves the halves.
|
||||
func Render(in Input, node string, mode Mode) (Rendered, error) {
|
||||
return newComposer(in).render(node, mode)
|
||||
}
|
||||
|
||||
func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
|
||||
resolved, err := c.planFor(nodeName)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
node, err := c.in.node(nodeName)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
plan := resolved.Plan
|
||||
var out Rendered
|
||||
foreseen := func(s string) { out.Foreseen = append(out.Foreseen, s) }
|
||||
|
||||
grants, withheld, unbound, grantsForeseen, err := c.grantsFor(nodeName)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
placed, err := c.places(nodeName, resolved)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
r := placed.Rendering
|
||||
out.ForeseenPorts = placed.ForeseenPorts
|
||||
r.Grants, r.Withheld, r.Unbound = grants, withheld, unbound
|
||||
out.Withheld, out.Unbound = withheld, unbound
|
||||
for _, g := range grantsForeseen {
|
||||
foreseen(g)
|
||||
}
|
||||
|
||||
// And each module's own secrets: a module handed to the provisioner has its secrets from the vault through the client.
|
||||
handing := catalogue.Rendering{Hands: r.Hands, Provisioner: r.Provisioner}
|
||||
needed, foreseenOwn, err := c.ownSecretsFor(node, plan, handing, mode)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
r.Needed = needed
|
||||
if mode == Foreseeing || len(foreseenOwn) > 0 {
|
||||
r.Foreseen = foreseenOwn
|
||||
}
|
||||
var names []string
|
||||
for module, secrets := range foreseenOwn {
|
||||
for name := range secrets {
|
||||
names = append(names, module+"/"+name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
foreseen(n)
|
||||
}
|
||||
|
||||
// The core's own credentials as the vault sealed them, for an engine that opens the vault's box.
|
||||
if node.Stated && node.Contract >= VaultBoxContract {
|
||||
sealed, notPlaced, wanted := c.coreBoxesFor(node, plan.Modules, handing)
|
||||
r.VaultSealed, r.NotPlaced = sealed, notPlaced
|
||||
out.CoreBoxesWanted = wanted
|
||||
}
|
||||
|
||||
// And a certificate for this machine's name inside the mesh, when anything on it asks.
|
||||
for _, m := range plan.Modules {
|
||||
if m.Certificate == nil {
|
||||
continue
|
||||
}
|
||||
certificate, authority, stood, err := c.certificateFor(node)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
r.Certificate, r.Authority = certificate, authority
|
||||
if stood {
|
||||
foreseen("the certificate and the authority of " + nodeName)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// The bus's user list, for the machine that runs the bus: names and hashes the Input carries, never passwords.
|
||||
if holdsTheBus(plan.Modules) {
|
||||
r.BusUsers = c.in.BusUsers
|
||||
if r.BusUsers == "" {
|
||||
r.BusUsers = ForeseenBusUsers
|
||||
foreseen("the bus's user list")
|
||||
}
|
||||
if len(c.in.BusMissing) > 0 {
|
||||
out.Said = append(out.Said, fmt.Sprintf("the bus's user list leaves out %d user(s) the mesh has minted no credential "+
|
||||
"for: %s. Each is a user that cannot connect until one is issued", len(c.in.BusMissing), strings.Join(c.in.BusMissing, ", ")))
|
||||
}
|
||||
}
|
||||
r.BusMembership = c.in.BusMemberships[nodeName]
|
||||
out.Rendering = r
|
||||
// And what the composition found on the way, which the controller writes to its log: a machine not counted as on the network.
|
||||
out.Said = c.saidBeside(out.Said)
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
)
|
||||
|
||||
// The contracts of the node-engine's statement that gate what a machine is sent, as the controller's link package names them
|
||||
// (mesh-controller internal/link/protocol.go at 6c2a8d62). An older engine parses strictly and refuses a field it does not
|
||||
// know, whole.
|
||||
const (
|
||||
ReadinessContract = 2 // the engine reads a resource's `health` (novox/hq ADR 0240)
|
||||
RootContract = 3 // the engine judges a user's declared `root` (ADR 0266)
|
||||
BusyContract = 8 // the engine reads a container's `busy` (ADR 0335)
|
||||
HandedContract = 11 // the engine writes a filled file on the provisioner client's word alone
|
||||
VaultBoxContract = 12 // the engine opens the vault's box
|
||||
)
|
||||
|
||||
// The provisioner client's seat and own secret (the controller's broker.NodeProvisionerSeat and ProvisionerSecret).
|
||||
const (
|
||||
NodeProvisionerSeat = "node-provisioner"
|
||||
ProvisionerSecret = "provisioner-broker"
|
||||
)
|
||||
|
||||
// ErrPortTaken is the controller's refusal for a machine port held twice on one machine.
|
||||
var ErrPortTaken = errors.New("that port is already held on this machine")
|
||||
|
||||
// Placed is the place half of a rendering (plan.go renderingFor): where a machine puts what its modules need reachable, the
|
||||
// seats and who holds them, the names of the mesh, the networks, accounts and the operator's export. It is a catalogue.Rendering
|
||||
// with those fields set, and the ports it could not read named foreseen. Nothing was allocated.
|
||||
type Placed struct {
|
||||
Rendering catalogue.Rendering
|
||||
// ForeseenPorts are the ports the mesh may assign that the Input holds none for, as "<module>:<port>": composed with the
|
||||
// manifest's own number as the Reading mode composes them, and never invented. The controller's adapter allocates and
|
||||
// composes again.
|
||||
ForeseenPorts []string
|
||||
}
|
||||
|
||||
// Places composes the place half of the rendering for a node (the controller's renderingFor in the Reading and Foreseeing modes,
|
||||
// without the credentials): over an Input, allocating nothing and changing nothing in it. The refusals are the controller's,
|
||||
// in its words.
|
||||
func Places(in Input, node string) (Placed, error) {
|
||||
c := newComposer(in)
|
||||
resolved, err := c.planFor(node)
|
||||
if err != nil {
|
||||
return Placed{}, err
|
||||
}
|
||||
return c.places(node, resolved)
|
||||
}
|
||||
|
||||
func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) {
|
||||
var out Placed
|
||||
node, err := c.in.node(nodeName)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
plan, settings := resolved.Plan, resolved.Settings
|
||||
r := &out.Rendering
|
||||
|
||||
// Where this machine puts what each module needs reachable (novox/hq ADR 0038): what it was already given, and the ports
|
||||
// it was given over those (ADR 0100).
|
||||
given, err := c.portsFor(node, plan, settings, r, &out.ForeseenPorts)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Hands: whether the engine writes a filled file on the provisioner client's word alone; engines older than a contract are
|
||||
// not sent the field they would refuse.
|
||||
r.Hands = takesHanded(node)
|
||||
r.Provisioner = provisionerAccount(plan)
|
||||
r.ReadsHealth = node.Stated && node.Contract >= ReadinessContract
|
||||
r.JudgesRoot = node.Stated && node.Contract >= RootContract
|
||||
r.ReadsBusy = node.Stated && node.Contract >= BusyContract
|
||||
|
||||
// And who else is on the private network, which is what a rule saying "from the mesh" resolves to.
|
||||
r.Mesh = c.onThePrivateNetwork()
|
||||
// The private network's range, offered to a module as ${machine:mesh-range}.
|
||||
r.MeshRange = c.overlayRange()
|
||||
|
||||
// The artifact store as this node reaches it now, with what the mesh has built.
|
||||
store, err := c.artifactStoreAddress(nodeName)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
r.ArtifactStore = store
|
||||
r.Built = make(map[string]bool, len(c.in.Built))
|
||||
for _, repository := range c.in.Built {
|
||||
r.Built[repository] = true
|
||||
}
|
||||
|
||||
// And every machine's name, so a container can reach one.
|
||||
r.Names = c.namesInTheMesh()
|
||||
|
||||
// Each machine's operator account, so an ssh Host block can name the login for every node (novox/hq to-be 29).
|
||||
r.Accounts = map[string]string{}
|
||||
for _, n := range c.in.sorted() {
|
||||
if n.Account != "" {
|
||||
r.Accounts[n.Name] = n.Account
|
||||
}
|
||||
}
|
||||
|
||||
// The roster is the machines and nothing else (novox/hq ADR 0191).
|
||||
r.Machines = make(map[string]string, len(r.Names))
|
||||
for name, at := range r.Names {
|
||||
r.Machines[name] = at
|
||||
}
|
||||
|
||||
// And every zone a module in the mesh answers itself (novox/hq ADR 0199).
|
||||
zones, err := c.zonesInTheMesh()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
r.Zones = zones
|
||||
|
||||
// And who holds each replicated seat, where (novox/hq ADR 0223).
|
||||
r.Holders = c.replicatedHolders()
|
||||
|
||||
r.Suffix, r.TunnelInterface = c.suffix(), overlay.Interface
|
||||
|
||||
// And, for a module that keeps them, every operator-sealed secret in the mesh (novox/hq ADR 0085, amended).
|
||||
for _, m := range plan.Modules {
|
||||
if m.Keeps == "" {
|
||||
continue
|
||||
}
|
||||
r.Kept = c.in.OperatorExport
|
||||
break
|
||||
}
|
||||
|
||||
// Whether this node is adopted (novox/hq ADR 0100), and which modules were taken there.
|
||||
r.Adopted = node.Adopted
|
||||
var taken map[string]bool
|
||||
if node.Adopted {
|
||||
taken = map[string]bool{}
|
||||
for _, m := range node.Taken {
|
||||
taken[m] = true
|
||||
}
|
||||
}
|
||||
r.Taken = taken
|
||||
|
||||
// Where this node put the foundation's servers (novox/hq 04-ISSUES/102).
|
||||
seats, err := c.seatsOn(node, plan.Modules, node.Adopted, taken)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
r.Seats = seats
|
||||
|
||||
// Which of this machine's links face outside (novox/hq ADR 0140), and which of its networks count as its local network
|
||||
// (ADR 0340).
|
||||
r.OutwardLinks = node.OutwardLinks
|
||||
r.LocalNetworks = catalogue.CountedLocalNetworks(localNetworksOf(node))
|
||||
|
||||
// Where this machine reaches each mesh seat's holder, for ${seat:<seat>:reach} (novox/hq ADR 0222).
|
||||
if store != "" {
|
||||
r.SeatReach = map[string]string{"mesh-artifact-store": store}
|
||||
}
|
||||
r.Settings = settings
|
||||
r.Given = given
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// takesHanded says whether a machine's newest statement is of an engine that writes a filled file on the provisioner client's
|
||||
// word alone.
|
||||
func takesHanded(n Node) bool { return n.Stated && n.Contract >= HandedContract }
|
||||
|
||||
// provisionerAccount is the account the provisioner's client runs as on this machine (novox/hq ADR 0349 clause 4): the user
|
||||
// the process of the module running it runs as, empty where no such module is on the machine.
|
||||
func provisionerAccount(plan catalogue.Resolution) string {
|
||||
for _, m := range plan.Modules {
|
||||
if !runsTheProvisionerClient(m) {
|
||||
continue
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if r["type"] == "process" {
|
||||
if user, _ := r["user"].(string); user != "" && user != "root" {
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// runsTheProvisionerClient says a module runs its machine's provisioner client: it claims the seat and declares the own
|
||||
// secret that account is delivered in.
|
||||
func runsTheProvisionerClient(m catalogue.Manifest) bool {
|
||||
_, reads := m.OwnSecrets[ProvisionerSecret]
|
||||
return reads && m.ClaimsSeat(NodeProvisionerSeat)
|
||||
}
|
||||
|
||||
// portsFor is where a node puts what each module needs reachable, in the Reading mode: the ports it was given win, then the
|
||||
// ones the mesh assigned, and a port the Input holds none for is composed as the manifest says and named foreseen. One holder
|
||||
// per machine port across the node's modules: settings written before this was refused are refused here.
|
||||
func (c *composer) portsFor(node Node, plan catalogue.Resolution, settings catalogue.SettingsBy, r *catalogue.Rendering,
|
||||
foreseen *[]string) (map[string]map[int]int, error) {
|
||||
already := node.Ports
|
||||
|
||||
given := map[string]map[int]int{}
|
||||
for _, m := range plan.Modules {
|
||||
g, err := catalogue.GivenPorts(m, settings[m.Module])
|
||||
if err != nil {
|
||||
// A given port its definition no longer publishes: the module is left out of the declaration, by name, when it is
|
||||
// composed (novox/hq ADR 0163, rule 6) -- never the machine refused here for it.
|
||||
continue
|
||||
}
|
||||
if g != nil {
|
||||
given[m.Module] = g
|
||||
}
|
||||
}
|
||||
holders := map[int]string{}
|
||||
for _, m := range plan.Modules {
|
||||
for _, at := range given[m.Module] {
|
||||
if other, twice := holders[at]; twice && other != m.Module {
|
||||
return nil, fmt.Errorf("%w: %s and %s are both given machine port %d on %s", ErrPortTaken, other, m.Module,
|
||||
at, node.Name)
|
||||
}
|
||||
holders[at] = m.Module
|
||||
}
|
||||
}
|
||||
|
||||
ports := map[string]map[int]int{}
|
||||
for _, m := range plan.Modules {
|
||||
for _, l := range m.Listens {
|
||||
if at, isGiven := given[m.Module][l.Port]; isGiven {
|
||||
if ports[m.Module] == nil {
|
||||
ports[m.Module] = map[int]int{}
|
||||
}
|
||||
ports[m.Module][l.Port] = at
|
||||
continue
|
||||
}
|
||||
// Only a port the module actually publishes is the mesh's to move. Recorded either way, because this map means where
|
||||
// this module's port is on this machine.
|
||||
where, mayAssign := m.MachineSide(l.Port)
|
||||
if mayAssign {
|
||||
// Whatever was chosen last time, and nothing if there was no last time.
|
||||
if at, known := already[m.Module][l.Port]; known {
|
||||
where = at
|
||||
} else {
|
||||
*foreseen = append(*foreseen, fmt.Sprintf("%s:%d", m.Module, l.Port))
|
||||
}
|
||||
}
|
||||
if ports[m.Module] == nil {
|
||||
ports[m.Module] = map[int]int{}
|
||||
}
|
||||
ports[m.Module][l.Port] = where
|
||||
}
|
||||
}
|
||||
r.Ports = ports
|
||||
return given, nil
|
||||
}
|
||||
|
||||
// onThePrivateNetwork is every node's address on the private network, sorted: the same set the names mean by it, so a rule
|
||||
// saying "from the mesh" admits exactly the machines the mesh names. A node with no address is left out, not rendered as an
|
||||
// empty source.
|
||||
func (c *composer) onThePrivateNetwork() []string {
|
||||
var out []string
|
||||
for _, p := range c.onTheNetwork() {
|
||||
if strings.TrimSpace(p.Place.Address) != "" {
|
||||
out = append(out, p.Place.Address)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// namesInTheMesh is every machine's internal name and the address behind it, for every machine on the private network, and
|
||||
// the carried peers the operator has named (novox/hq issue 112).
|
||||
func (c *composer) namesInTheMesh() map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, p := range c.onTheNetwork() {
|
||||
out[c.internalName(p.Name)] = p.Place.Address
|
||||
}
|
||||
for _, p := range c.in.CarriedPeers {
|
||||
if p.Named == "" || p.EnrolledAs != "" {
|
||||
continue
|
||||
}
|
||||
if _, taken := out[c.internalName(p.Named)]; taken {
|
||||
continue // a node of the mesh owns the name; the stale statement loses
|
||||
}
|
||||
out[c.internalName(p.Named)] = p.Address
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the port that machine put it on.
|
||||
func (c *composer) artifactStoreOnNetwork(on map[string]bool) (node, port string, found bool, err error) {
|
||||
providers := map[string]catalogue.Manifest{}
|
||||
for name, m := range c.in.Catalogue {
|
||||
if _, offers := m.Serves[catalogue.ArtifactStoreProvision]; offers {
|
||||
providers[name] = m
|
||||
}
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
return "", "", false, nil
|
||||
}
|
||||
// In a stated order, so two machines offering it would always answer the same one.
|
||||
machines := make([]string, 0, len(on))
|
||||
for machine := range on {
|
||||
machines = append(machines, machine)
|
||||
}
|
||||
sort.Strings(machines)
|
||||
for _, machine := range machines {
|
||||
n, err := c.in.node(machine)
|
||||
if err != nil {
|
||||
return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err)
|
||||
}
|
||||
for _, a := range n.Assigned {
|
||||
m, offers := providers[a]
|
||||
if !offers {
|
||||
continue
|
||||
}
|
||||
serves, _, err := c.servedOnNode(machine, m, catalogue.ArtifactStoreProvision)
|
||||
if err != nil {
|
||||
return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err)
|
||||
}
|
||||
if p, ok := serves["port"]; ok {
|
||||
return machine, fmt.Sprintf("%v", p), true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", "", false, nil
|
||||
}
|
||||
|
||||
// artifactStoreAddress is the artifact store as `forNode` reaches it: `<node>.internal:<port>` over the private network, or
|
||||
// `127.0.0.1:<port>` for the node that holds the store itself when nothing is on the network yet, and empty for any other
|
||||
// (novox/hq 04-ISSUES/102).
|
||||
func (c *composer) artifactStoreAddress(forNode string) (string, error) {
|
||||
onNetwork, err := c.whereEveryoneIs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
on := map[string]bool{}
|
||||
for name := range onNetwork {
|
||||
on[name] = true
|
||||
}
|
||||
node, port, found, err := c.artifactStoreOnNetwork(on)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if found {
|
||||
return c.internalName(node) + ":" + port, nil
|
||||
}
|
||||
all := map[string]bool{}
|
||||
for _, n := range c.in.Nodes {
|
||||
all[n.Name] = true
|
||||
}
|
||||
holder, port, found, err := c.artifactStoreOnNetwork(all)
|
||||
if err != nil || !found || holder != forNode {
|
||||
return "", err
|
||||
}
|
||||
return "127.0.0.1:" + port, nil
|
||||
}
|
||||
|
||||
// zonesInTheMesh is every zone a module in the mesh declares, where the mesh placed it (novox/hq ADR 0199): the zone settled
|
||||
// from that node's settings, the node's private address, the port the answering listen is published on there. A node whose set
|
||||
// does not compose declares nothing and is passed over; what the mesh refuses about the zones together is refused, by name.
|
||||
func (c *composer) zonesInTheMesh() ([]catalogue.ZoneAt, error) {
|
||||
address := map[string]string{}
|
||||
for _, n := range c.in.sorted() {
|
||||
if strings.TrimSpace(n.Place.Address) != "" {
|
||||
address[n.Name] = n.Place.Address
|
||||
}
|
||||
}
|
||||
var zones []catalogue.ZoneAt
|
||||
var public []string
|
||||
for _, n := range c.in.sorted() {
|
||||
resolved, err := c.planFor(n.Name)
|
||||
switch {
|
||||
case unresolvable(err):
|
||||
continue
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("the zones %s answers cannot be read: %w", n.Name, err)
|
||||
}
|
||||
plan := resolved.Plan
|
||||
if plan.PublicDomain != "" {
|
||||
public = append(public, plan.PublicDomain)
|
||||
}
|
||||
for _, m := range plan.Modules {
|
||||
if m.Zone == nil {
|
||||
continue
|
||||
}
|
||||
at := address[n.Name]
|
||||
if at == "" {
|
||||
// Not on the private network yet: nothing could reach its answerer.
|
||||
continue
|
||||
}
|
||||
published, layers, err := c.portsGivenOn(n.Name, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the zone %s declares on %s cannot be read: %w", m.Module, n.Name, err)
|
||||
}
|
||||
z, err := catalogue.ZoneOn(m, layers, published, n.Name, at)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zones = append(zones, *z)
|
||||
}
|
||||
}
|
||||
if problems := catalogue.ZonesProblems(zones, c.suffix(), public); len(problems) > 0 {
|
||||
return nil, fmt.Errorf("the mesh's zones cannot be forwarded:\n - %s", strings.Join(problems, "\n - "))
|
||||
}
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
// replicatedHolders is, for each replicated mesh seat, every machine on the private network holding it, by internal name at its
|
||||
// private address (novox/hq ADR 0223): the holders on record, and only the sole claimant when there are none. A holder off
|
||||
// the private network is left out.
|
||||
func (c *composer) replicatedHolders() map[string]map[string]string {
|
||||
var replicated []catalogue.Seat
|
||||
for _, s := range catalogue.Seats() {
|
||||
if s.Replicated && s.Scope == catalogue.ScopeMesh {
|
||||
replicated = append(replicated, s)
|
||||
}
|
||||
}
|
||||
if len(replicated) == 0 {
|
||||
return nil
|
||||
}
|
||||
address := map[string]string{}
|
||||
for _, p := range c.onTheNetwork() {
|
||||
address[p.Name] = p.Place.Address
|
||||
}
|
||||
// Every module of the catalogue with the nodes it is assigned on, sorted, as the controller's Catalogued answers.
|
||||
on := map[string][]string{}
|
||||
for _, n := range c.in.sorted() {
|
||||
for _, module := range n.Assigned {
|
||||
on[module] = append(on[module], n.Name)
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(c.in.Catalogue))
|
||||
for name := range c.in.Catalogue {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
out := map[string]map[string]string{}
|
||||
for _, seat := range replicated {
|
||||
var nodes []string
|
||||
for _, h := range c.in.Holdings {
|
||||
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name && h.Scope == seat.Scope {
|
||||
nodes = append(nodes, h.Node)
|
||||
}
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
var derived []string
|
||||
for _, name := range names {
|
||||
for _, cl := range c.in.Catalogue[name].Claims {
|
||||
if cs, ok := catalogue.SeatNamed(cl.Name); ok && cs.Name == seat.Name && cl.At() == seat.Scope {
|
||||
derived = append(derived, on[name]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(derived) == 1 {
|
||||
nodes = derived
|
||||
}
|
||||
}
|
||||
at := map[string]string{}
|
||||
for _, n := range nodes {
|
||||
if address[n] != "" {
|
||||
at[c.internalName(n)] = address[n]
|
||||
}
|
||||
}
|
||||
out[seat.Name] = at
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// seatsOn is where a node put the holder of each mesh-scoped seat, by seat and by the port the holder's software uses: what
|
||||
// ${seat:...} answers with (novox/hq 04-ISSUES/102). Read for every module in the catalogue that claims a seat, in this node's set
|
||||
// or not; a holder in this node's set wins over one that is not.
|
||||
func (c *composer) seatsOn(node Node, inSet []catalogue.Manifest, adopted bool, taken map[string]bool) (map[string]map[int]int, error) {
|
||||
assigned := map[string]bool{}
|
||||
for _, m := range inSet {
|
||||
assigned[m.Module] = true
|
||||
}
|
||||
names := make([]string, 0, len(c.in.Catalogue))
|
||||
for name := range c.in.Catalogue {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
seats := map[string]map[int]int{}
|
||||
for _, name := range names {
|
||||
m := c.in.Catalogue[name]
|
||||
var claims []string
|
||||
for _, cl := range m.Claims {
|
||||
if cl.At() == catalogue.ScopeMesh {
|
||||
claims = append(claims, cl.Name)
|
||||
}
|
||||
}
|
||||
if len(claims) == 0 {
|
||||
continue
|
||||
}
|
||||
// Only a port the node was given or the mesh assigned, never the manifest's own number. And on an adopted node a holder
|
||||
// assigned but not yet taken is the found container, so its mesh-assigned ports do not count there; a given port does.
|
||||
ports, _, err := c.portsGivenOn(node.Name, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if adopted && !taken[name] {
|
||||
layers := node.Settings[m.Module]
|
||||
ports = map[int]int{}
|
||||
if given, err := catalogue.GivenPorts(m, layers); err == nil {
|
||||
ports = given
|
||||
}
|
||||
}
|
||||
if len(ports) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, seat := range claims {
|
||||
if seats[seat] == nil {
|
||||
seats[seat] = map[int]int{}
|
||||
}
|
||||
for wanted, at := range ports {
|
||||
if _, said := seats[seat][wanted]; said && !assigned[name] {
|
||||
continue
|
||||
}
|
||||
seats[seat][wanted] = at
|
||||
}
|
||||
}
|
||||
}
|
||||
return seats, nil
|
||||
}
|
||||
|
||||
// localNetworksOf is a machine's attached networks judged (novox/hq ADR 0340): which count as its local network, from what it
|
||||
// last said of itself and the links it said face outside it. A machine that has said none judges to nothing.
|
||||
func localNetworksOf(n Node) []catalogue.LocalNetwork {
|
||||
if !n.NetworksSaid {
|
||||
return nil
|
||||
}
|
||||
return catalogue.JudgeLocalNetworks(n.Networks, n.OutwardLinks, overlay.Interface)
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
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.shelfFor(node)
|
||||
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)
|
||||
if licence.Keys == nil {
|
||||
// The keys are not given: composed with a stand-in and named, not failed for a value only a send reads.
|
||||
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))
|
||||
} else {
|
||||
// Given, and none for this holder: the mesh holds none.
|
||||
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.
|
||||
func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
|
||||
node, err := c.in.node(nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
}
|
||||
shelf := c.shelfFor(node)
|
||||
world, _, _, err := c.theRestOfTheMesh(nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
}
|
||||
world.Pinned = node.Pins
|
||||
// Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232).
|
||||
world.Bound = node.Bound
|
||||
|
||||
onNetwork, err := c.whereEveryoneIs()
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
}
|
||||
|
||||
// What this mesh can answer with a record rather than a machine, and which record each of this node's modules was
|
||||
// put on.
|
||||
world.Licences, world.Using = c.licencesFor(nodeName)
|
||||
|
||||
resolved, err := catalogue.Resolve(shelf, node.Assigned,
|
||||
catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities,
|
||||
At: onNetwork[nodeName], PublicDomain: node.PublicDomain,
|
||||
Account: node.Account, AccountHome: node.AccountHome,
|
||||
AgentAccount: node.AgentAccount, AgentAccountHome: node.AgentAccountHome}, world)
|
||||
if err != nil {
|
||||
// The node's own set does not compose: the only failure a mesh-wide gatherer may pass over.
|
||||
return catalogue.Resolution{}, notResolvable{err}
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the
|
||||
// build it runs put in place of the current one (plan.go keepRecorded). A copy: the Input's catalogue is untouched. Only the node's own planFor and
|
||||
// resolution use it; the rest of the mesh and who is on the network are read with the current manifests, as the controller reads them
|
||||
// before it keeps a recorded module.
|
||||
func (c *composer) shelfFor(node Node) map[string]catalogue.Manifest {
|
||||
if len(node.Kept) == 0 {
|
||||
return c.in.Catalogue
|
||||
}
|
||||
shelf := make(map[string]catalogue.Manifest, len(c.in.Catalogue)+len(node.Kept))
|
||||
for name, m := range c.in.Catalogue {
|
||||
shelf[name] = m
|
||||
}
|
||||
for name, m := range node.Kept {
|
||||
shelf[name] = m
|
||||
}
|
||||
return shelf
|
||||
}
|
||||
|
||||
// licencesFor is what this node can be answered with by record, and what it was put on. A mesh with no licences at all
|
||||
// is the ordinary case and not an error.
|
||||
func (c *composer) licencesFor(node string) (map[string][]catalogue.Record, map[string]map[string]catalogue.Record) {
|
||||
all := c.in.Licences
|
||||
if len(all) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
offered := map[string][]catalogue.Record{}
|
||||
byName := map[string]catalogue.Record{}
|
||||
for _, one := range all {
|
||||
record := catalogue.Record{Name: one.Name, Serves: one.Serves}
|
||||
offered[LicenceProvision] = append(offered[LicenceProvision], record)
|
||||
byName[one.Name] = record
|
||||
}
|
||||
using := map[string]map[string]catalogue.Record{}
|
||||
for _, one := range all {
|
||||
for _, h := range one.Holders {
|
||||
if h.Node != node {
|
||||
continue
|
||||
}
|
||||
if using[h.Module] == nil {
|
||||
using[h.Module] = map[string]catalogue.Record{}
|
||||
}
|
||||
using[h.Module][LicenceProvision] = byName[one.Name]
|
||||
}
|
||||
}
|
||||
return offered, using
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// notResolvable marks the one failure in a node's resolution that is a statement about the node: its assigned modules
|
||||
// do not compose. Every other failure means the mesh could not be asked. Whoever gathers across the mesh skips a node
|
||||
// that cannot resolve and never a node that could not be read (novox/hq 04-ISSUES/152).
|
||||
type notResolvable struct{ err error }
|
||||
|
||||
func (n notResolvable) Error() string { return n.err.Error() }
|
||||
func (n notResolvable) Unwrap() error { return n.err }
|
||||
|
||||
// unresolvable reports whether err is a node's own set failing to compose.
|
||||
func unresolvable(err error) bool {
|
||||
var n notResolvable
|
||||
return errors.As(err, &n)
|
||||
}
|
||||
|
||||
// Passes is what a composition found that the mesh must record, and did not: each seat held once whose holder on record
|
||||
// is assigned nowhere that exactly one claimant takes over (novox/hq ADR 0449 clause 4), and the lines that say what was
|
||||
// found. The caller records a pass and writes its hand act; this package does neither.
|
||||
type Passes struct {
|
||||
// Passes are the seats to pass, in the order they were found.
|
||||
Passes []catalogue.SeatPass
|
||||
// Said are the lines the finding says (a seat two claimants could take, one nothing can).
|
||||
Said []string
|
||||
}
|
||||
|
||||
// LeftOut is a node that does not resolve in the view of the rest of the mesh, with why: it offers nothing and holds
|
||||
// nothing as far as every other machine's plan can tell (novox/hq issue 188).
|
||||
type LeftOut struct {
|
||||
Node string
|
||||
Err error
|
||||
}
|
||||
|
||||
// String is the line the controller says of it.
|
||||
func (l LeftOut) String() string {
|
||||
return fmt.Sprintf("%s is left out of the rest of the mesh: it does not resolve: %v", l.Node, l.Err)
|
||||
}
|
||||
|
||||
// TheRestOfTheMesh is what every other node holds and offers, as the controller works it out (plan.go theRestOfTheMesh):
|
||||
// two passes over the others, because what a node offers needs it resolved and resolving it may need what the mesh
|
||||
// offers. The first takes brokered requirements on trust and answers only what each node offers; the second answers
|
||||
// everything with that in hand. Nothing is declared from the first.
|
||||
//
|
||||
// It writes nothing. A seat the mesh passes on is returned in Passes for the caller to record; the World carries the
|
||||
// holdings as they stand after the passes, and Input's own are unchanged. A node left out is returned with its reason, in
|
||||
// the order the controller said them (the first pass's, then the second's when no node is excluded).
|
||||
func TheRestOfTheMesh(in Input, exclude string) (catalogue.World, Passes, []LeftOut, error) {
|
||||
c := newComposer(in)
|
||||
return c.theRestOfTheMesh(exclude)
|
||||
}
|
||||
|
||||
func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, []LeftOut, error) {
|
||||
shelf := c.in.Catalogue
|
||||
holdings := append([]catalogue.Held(nil), c.in.Holdings...)
|
||||
nodes := c.in.sorted()
|
||||
|
||||
onNetwork, err := c.whereEveryoneIs()
|
||||
if err != nil {
|
||||
return catalogue.World{}, Passes{}, nil, err
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
node catalogue.Node
|
||||
assigned []string
|
||||
pins map[string]catalogue.Chosen
|
||||
}
|
||||
var others []candidate
|
||||
// Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4): a seat
|
||||
// held once whose holder on record is assigned on no machine passes to its only claimant here.
|
||||
everyAssigned := map[string][]string{}
|
||||
for _, n := range nodes {
|
||||
if len(n.Assigned) > 0 {
|
||||
everyAssigned[n.Name] = n.Assigned
|
||||
}
|
||||
}
|
||||
var passes Passes
|
||||
holdings, passes = passSeatsOn(holdings, assignmentsOf(shelf, everyAssigned))
|
||||
for _, n := range nodes {
|
||||
if n.Name == exclude {
|
||||
continue
|
||||
}
|
||||
theirs := everyAssigned[n.Name]
|
||||
if len(theirs) == 0 {
|
||||
continue
|
||||
}
|
||||
others = append(others, candidate{
|
||||
node: catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities,
|
||||
At: onNetwork[n.Name], PublicDomain: n.PublicDomain},
|
||||
assigned: theirs, pins: n.Pins})
|
||||
}
|
||||
|
||||
var left []LeftOut
|
||||
offered := map[string][]catalogue.Provider{}
|
||||
var firstHeld []catalogue.Held
|
||||
for _, o := range others {
|
||||
got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
|
||||
if err != nil {
|
||||
// Said, not skipped: a machine dropped here offers nothing and holds nothing.
|
||||
left = append(left, LeftOut{Node: o.node.Name, Err: err})
|
||||
continue
|
||||
}
|
||||
firstHeld = append(firstHeld, got.Claims...)
|
||||
for _, m := range got.Modules {
|
||||
for _, name := range m.OffersAt(catalogue.ScopeMesh) {
|
||||
// What that module says a consumer needs to know, with that node's settings on it.
|
||||
serves, layers, err := c.servedOnNode(o.node.Name, m, name)
|
||||
if err != nil {
|
||||
return catalogue.World{}, Passes{}, nil, err
|
||||
}
|
||||
// And its own composed names, as that node composes them (novox/hq ADR 0346).
|
||||
serves, leftOut, err := got.ServedOwnNames(m, name, serves, catalogue.WithDefaults(m, layers))
|
||||
if err != nil {
|
||||
return catalogue.World{}, Passes{}, nil, err
|
||||
}
|
||||
offered[name] = append(offered[name], catalogue.Provider{
|
||||
Node: o.node.Name, At: o.node.At, Serves: serves, Module: m.Module, LeftOut: leftOut})
|
||||
}
|
||||
}
|
||||
}
|
||||
for k := range offered {
|
||||
sort.Slice(offered[k], func(i, j int) bool {
|
||||
return offered[k][i].Node < offered[k][j].Node
|
||||
})
|
||||
}
|
||||
|
||||
// The second pass is given the first pass's holdings (novox/hq ADR 0110).
|
||||
world := catalogue.World{Offered: offered, Held: firstHeld, Holdings: holdings}
|
||||
var held []catalogue.Held
|
||||
for _, o := range others {
|
||||
// Each machine is resolved with its own pins, as its plan is.
|
||||
theirs := world
|
||||
theirs.Pinned = o.pins
|
||||
got, err := catalogue.Resolve(shelf, o.assigned, o.node, theirs)
|
||||
if err != nil {
|
||||
// Said only for the whole-mesh view: with one machine excluded, the view is partial by design.
|
||||
if exclude == "" {
|
||||
left = append(left, LeftOut{Node: o.node.Name, Err: err})
|
||||
}
|
||||
continue
|
||||
}
|
||||
held = append(held, got.Claims...)
|
||||
}
|
||||
world.Held = held
|
||||
return world, passes, left, nil
|
||||
}
|
||||
|
||||
// passSeatsOn finds, in the composition that finds it, the new holder of each seat held once whose holder on record is
|
||||
// assigned nowhere and which has exactly one claimant able to hold it (novox/hq ADR 0449 clause 4), and answers the
|
||||
// holdings as they are after it, with the passes. Nothing is recorded.
|
||||
func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) ([]catalogue.Held, Passes) {
|
||||
passes, said := catalogue.SeatsToPassOn(holdings, assignments)
|
||||
out := Passes{Said: said}
|
||||
for _, p := range passes {
|
||||
out.Passes = append(out.Passes, p)
|
||||
var next []catalogue.Held
|
||||
for _, h := range holdings {
|
||||
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == p.Seat.Name {
|
||||
continue
|
||||
}
|
||||
next = append(next, h)
|
||||
}
|
||||
p.To.Site = p.From.Site
|
||||
holdings = append(next, p.To)
|
||||
}
|
||||
return holdings, out
|
||||
}
|
||||
|
||||
// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. A module assigned and
|
||||
// missing from the shelf is still assigned: an assignment with no claims, so its holder is not taken for gone.
|
||||
func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment {
|
||||
var out []catalogue.Assignment
|
||||
names := make([]string, 0, len(assigned))
|
||||
for node := range assigned {
|
||||
names = append(names, node)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, node := range names {
|
||||
for _, module := range assigned[node] {
|
||||
m, ok := shelf[module]
|
||||
if !ok {
|
||||
m = catalogue.Manifest{Module: module}
|
||||
}
|
||||
out = append(out, catalogue.Assignment{Node: node, Manifest: m})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// servedOnNode is what a module on a node tells a consumer of one of its provisions, with THAT node's ports on it: the
|
||||
// port the node was given over the one the mesh assigned over the manifest's own, settled with the node's settings
|
||||
// layers. The one derivation for every reader of a provider's address.
|
||||
func (c *composer) servedOnNode(node string, m catalogue.Manifest, provision string) (map[string]any, []catalogue.Layer, error) {
|
||||
ports, layers, err := c.portsGivenOn(node, m)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
serves := catalogue.ServedOn(m, provision, ports)
|
||||
if len(serves) > 0 {
|
||||
serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return serves, layers, nil
|
||||
}
|
||||
|
||||
// portsGivenOn is where a node puts a module's ports — assigned, then given over them — and the node's settings layers
|
||||
// for the module.
|
||||
func (c *composer) portsGivenOn(node string, m catalogue.Manifest) (map[int]int, []catalogue.Layer, error) {
|
||||
n, err := c.in.node(node)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ports := map[int]int{}
|
||||
for wanted, at := range n.Ports[m.Module] {
|
||||
ports[wanted] = at
|
||||
}
|
||||
layers := n.Settings[m.Module]
|
||||
if given, err := catalogue.GivenPorts(m, layers); err == nil {
|
||||
for wanted, at := range given {
|
||||
ports[wanted] = at
|
||||
}
|
||||
}
|
||||
return ports, layers, nil
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Busy is how a long-running container says it holds work that would be lost if it were stopped now
|
||||
// (novox/hq ADR 0335 §1): a command run inside it, by its shell, as an exec health check is, that exits 0
|
||||
// while it is busy and non-zero when it is idle. Being asked is also the container's notice that it is
|
||||
// about to be replaced: from the first ask it takes no new work, so its answer ends once the work in hand
|
||||
// ends.
|
||||
//
|
||||
// **Not part of what the container is.** Busy is not in the container's spec: a changed command changes
|
||||
// when the container is replaced, never whether — a container is not recreated for its busy command alone.
|
||||
type Busy struct {
|
||||
Command string `json:"command"`
|
||||
// Timeout is how long one ask may take; empty is BusyTimeoutDefault.
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// The bounds of an ask (novox/hq ADR 0335).
|
||||
const (
|
||||
BusyTimeoutDefault = 10 * time.Second
|
||||
// BusyTimeoutCeiling is the longest one ask may take: an ask runs inside an apply, and an apply waits
|
||||
// for nothing longer than a look.
|
||||
BusyTimeoutCeiling = time.Minute
|
||||
// BusyBound is how long a container that says busy is left running after the first apply that would
|
||||
// have replaced it (ADR 0335 §3). The node-engine's, not the module's: no module holds its own
|
||||
// replacement for ever.
|
||||
BusyBound = 15 * time.Minute
|
||||
)
|
||||
|
||||
// UnmarshalJSON reads a busy strictly, as everything a declaration carries is read.
|
||||
func (b *Busy) UnmarshalJSON(raw []byte) error {
|
||||
type plain Busy
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("busy: %w", err)
|
||||
}
|
||||
*b = Busy(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Within is how long one ask may take. Validated on arrival, so an unreadable timeout is the default.
|
||||
func (b *Busy) Within() time.Duration {
|
||||
if d, err := time.ParseDuration(b.Timeout); err == nil && d > 0 {
|
||||
return d
|
||||
}
|
||||
return BusyTimeoutDefault
|
||||
}
|
||||
|
||||
// problems holds a container's busy to its shape: a one-line command, a timeout within its bounds, and only
|
||||
// on a container that stays up.
|
||||
func (b *Busy) problems(where string, longRunning bool) []string {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, where+": "+fmt.Sprintf(format, args...))
|
||||
}
|
||||
if !longRunning {
|
||||
say("busy is asked of a container that stays up; a step or a scheduled run is never replaced mid-run")
|
||||
}
|
||||
if strings.TrimSpace(b.Command) == "" {
|
||||
say("busy needs a command")
|
||||
}
|
||||
if strings.ContainsAny(b.Command, "\n\r") {
|
||||
say("busy's command is one line")
|
||||
}
|
||||
if b.Timeout != "" {
|
||||
d, err := time.ParseDuration(b.Timeout)
|
||||
switch {
|
||||
case err != nil:
|
||||
say("busy's timeout is a duration, not %q", b.Timeout)
|
||||
case d <= 0 || d > BusyTimeoutCeiling:
|
||||
say("an ask of busy takes more than nothing and at most %s, not %s", BusyTimeoutCeiling, d)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
package declaration
|
||||
|
||||
// A minimal five-field cron, enough to say when a scheduled step is due (novox/hq ADR 0053).
|
||||
//
|
||||
// **Written rather than pulled in.** The host depends on nothing it does not have to
|
||||
// (novox/hq ADR 0005), and a scheduled step needs exactly two questions answered — *is this minute
|
||||
// a match* and *when is the next one* — over the ordinary five fields (minute, hour, day-of-month,
|
||||
// month, day-of-week) with `*`, lists (`,`), ranges (`-`) and steps (`/`). That is small enough to
|
||||
// keep in the vocabulary the host already owns, and a cron library would be a dependency carried
|
||||
// for the parts of it nobody here uses.
|
||||
//
|
||||
// **The host both validates and evaluates.** The control plane refuses a malformed cron near its
|
||||
// author (novox/hq ADR 0053), and the host refuses it again on arrival for the same near-versus-far
|
||||
// reason every other field is checked here: a declaration the host does not fully understand is
|
||||
// refused whole rather than half-applied. The evaluation half — `Next` — is what the scheduler
|
||||
// fires on.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron is a parsed five-field expression, each field held as a bitset of the values it permits.
|
||||
//
|
||||
// The two day fields carry a flag for whether they were written as a bare `*`, because standard
|
||||
// cron gives them a special rule: when day-of-month and day-of-week are *both* restricted, a day
|
||||
// matches if *either* does; when one is `*`, only the other is consulted. Getting that wrong is the
|
||||
// classic cron surprise, so the flag is kept rather than rediscovered.
|
||||
type Cron struct {
|
||||
minute uint64
|
||||
hour uint64
|
||||
dom uint64
|
||||
month uint64
|
||||
dow uint64
|
||||
domStar bool
|
||||
dowStar bool
|
||||
}
|
||||
|
||||
// ParseCron reads a five-field cron expression, or says why it is not one.
|
||||
func ParseCron(expr string) (*Cron, error) {
|
||||
fields := strings.Fields(expr)
|
||||
if len(fields) != 5 {
|
||||
return nil, fmt.Errorf(
|
||||
"a schedule is a five-field cron expression (minute hour day-of-month month "+
|
||||
"day-of-week), and %q has %d field(s)", expr, len(fields))
|
||||
}
|
||||
|
||||
minute, _, err := parseCronField(fields[0], 0, 59)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule minute field: %w", err)
|
||||
}
|
||||
hour, _, err := parseCronField(fields[1], 0, 23)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule hour field: %w", err)
|
||||
}
|
||||
dom, domStar, err := parseCronField(fields[2], 1, 31)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule day-of-month field: %w", err)
|
||||
}
|
||||
month, _, err := parseCronField(fields[3], 1, 12)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule month field: %w", err)
|
||||
}
|
||||
// Day-of-week is 0-6 with Sunday at 0, and 7 is also accepted for Sunday — the convention every
|
||||
// cron keeps, so a crontab copied from elsewhere is not refused for saying 7.
|
||||
dow, dowStar, err := parseCronField(fields[4], 0, 7)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule day-of-week field: %w", err)
|
||||
}
|
||||
if dow&(1<<7) != 0 {
|
||||
dow |= 1 << 0
|
||||
dow &^= 1 << 7
|
||||
}
|
||||
|
||||
return &Cron{
|
||||
minute: minute, hour: hour, dom: dom, month: month, dow: dow,
|
||||
domStar: domStar, dowStar: dowStar,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCronField turns one field into the bitset of values it permits, and reports whether it was a
|
||||
// bare `*` (which the day fields treat specially).
|
||||
func parseCronField(spec string, min, max int) (uint64, bool, error) {
|
||||
if spec == "" {
|
||||
return 0, false, fmt.Errorf("is empty")
|
||||
}
|
||||
star := spec == "*"
|
||||
|
||||
var bits uint64
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
if part == "" {
|
||||
return 0, false, fmt.Errorf("%q has an empty element between commas", spec)
|
||||
}
|
||||
|
||||
// A step may follow either `*` or a range: `*/15`, `0-30/5`.
|
||||
step := 1
|
||||
rangePart := part
|
||||
if slash := strings.IndexByte(part, '/'); slash >= 0 {
|
||||
rangePart = part[:slash]
|
||||
n, err := strconv.Atoi(part[slash+1:])
|
||||
if err != nil || n < 1 {
|
||||
return 0, false, fmt.Errorf("step in %q is not a positive number", part)
|
||||
}
|
||||
step = n
|
||||
}
|
||||
|
||||
lo, hi := min, max
|
||||
switch {
|
||||
case rangePart == "*":
|
||||
// Full range, already set.
|
||||
case strings.IndexByte(rangePart, '-') >= 0:
|
||||
dash := strings.IndexByte(rangePart, '-')
|
||||
a, errA := strconv.Atoi(rangePart[:dash])
|
||||
b, errB := strconv.Atoi(rangePart[dash+1:])
|
||||
if errA != nil || errB != nil {
|
||||
return 0, false, fmt.Errorf("range %q is not two numbers", rangePart)
|
||||
}
|
||||
lo, hi = a, b
|
||||
default:
|
||||
n, err := strconv.Atoi(rangePart)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("%q is not a number", rangePart)
|
||||
}
|
||||
lo, hi = n, n
|
||||
}
|
||||
|
||||
if lo < min || hi > max || lo > hi {
|
||||
return 0, false, fmt.Errorf(
|
||||
"%q is outside the allowed range %d-%d", part, min, max)
|
||||
}
|
||||
for v := lo; v <= hi; v += step {
|
||||
bits |= 1 << uint(v)
|
||||
}
|
||||
}
|
||||
return bits, star, nil
|
||||
}
|
||||
|
||||
// Matches reports whether a scheduled step is due at this minute.
|
||||
func (c *Cron) Matches(t time.Time) bool {
|
||||
if c.minute&(1<<uint(t.Minute())) == 0 {
|
||||
return false
|
||||
}
|
||||
if c.hour&(1<<uint(t.Hour())) == 0 {
|
||||
return false
|
||||
}
|
||||
if c.month&(1<<uint(int(t.Month()))) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
domMatch := c.dom&(1<<uint(t.Day())) != 0
|
||||
dowMatch := c.dow&(1<<uint(int(t.Weekday()))) != 0
|
||||
// The standard day rule: both restricted means either may match; one as `*` defers to the other.
|
||||
switch {
|
||||
case c.domStar && c.dowStar:
|
||||
return true
|
||||
case c.domStar:
|
||||
return dowMatch
|
||||
case c.dowStar:
|
||||
return domMatch
|
||||
default:
|
||||
return domMatch || dowMatch
|
||||
}
|
||||
}
|
||||
|
||||
// Next is the first minute strictly after t at which the step is due, and false if there is none
|
||||
// within a bound generous enough to cover a once-a-year, leap-day schedule.
|
||||
//
|
||||
// Minute-by-minute rather than a closed form: the day rule above makes a closed form fiddly and
|
||||
// error-prone, and this is called once per fire — rarely — so simple and obviously correct wins.
|
||||
func (c *Cron) Next(after time.Time) (time.Time, bool) {
|
||||
t := after.Truncate(time.Minute).Add(time.Minute)
|
||||
// Five years of minutes: enough that "29 2 * * *" (Feb 29) always finds its next leap year.
|
||||
for i := 0; i < 5*366*24*60; i++ {
|
||||
if c.Matches(t) {
|
||||
return t, true
|
||||
}
|
||||
t = t.Add(time.Minute)
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
-2607
File diff suppressed because it is too large
Load Diff
-188
@@ -1,188 +0,0 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Health is how a long-running resource is ready, as the controller composed it from the module's
|
||||
// `health` (novox/hq ADR 0240 rule 2, to-be 48 §2–§3, Phase B): one kind and its timing, the endpoint
|
||||
// already the port this machine published it on.
|
||||
//
|
||||
// **The node-engine runs every kind and owns every verdict.** http and tcp it makes itself, from the
|
||||
// machine to the port; unit it reads from the service manager it already reads; exec and runtime it hands
|
||||
// to the container runtime as the container's own check, with this timing, and reads the state; tool it
|
||||
// asks of its own node tools. Nothing else on the machine sets a container's check.
|
||||
//
|
||||
// Refused here as the controller refuses it near the author, in the same bounds: an engine that took a
|
||||
// check it could not judge would say a module ready that nothing looked at.
|
||||
type Health struct {
|
||||
Kind string `json:"kind"`
|
||||
// Endpoint is the module's name for what Port is: for the words a verdict is said in.
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Tool string `json:"tool,omitempty"`
|
||||
Interval string `json:"interval"`
|
||||
Timeout string `json:"timeout"`
|
||||
Looks int `json:"looks"`
|
||||
Grace string `json:"grace"`
|
||||
// Needs is the provision the check exercises (to-be 48 §6): said with every verdict, so the
|
||||
// controller can hold what it finds under the provider's own condition.
|
||||
Needs string `json:"needs,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads a health strictly, as everything a declaration carries is read: a field this host
|
||||
// does not know is a part of the check the controller believes it asked for, and nothing would look at it.
|
||||
func (h *Health) UnmarshalJSON(raw []byte) error {
|
||||
type plain Health
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("health: %w", err)
|
||||
}
|
||||
*h = Health(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// The kinds.
|
||||
const (
|
||||
HealthRuntime = "runtime"
|
||||
HealthHTTP = "http"
|
||||
HealthTCP = "tcp"
|
||||
HealthExec = "exec"
|
||||
HealthUnit = "unit"
|
||||
HealthTool = "tool"
|
||||
)
|
||||
|
||||
// The bounds (ADR 0240 rule 2) — the controller's, held again here.
|
||||
const (
|
||||
HealthIntervalFloor = 10 * time.Second
|
||||
HealthLooksFloor = 2
|
||||
HealthWithin = 5 * time.Minute
|
||||
)
|
||||
|
||||
// Every, Within and GraceOf are the timing, read. Validated on arrival, so a parse error here is
|
||||
// impossible on a declaration that was accepted; it reads as zero.
|
||||
func (h *Health) Every() time.Duration { d, _ := time.ParseDuration(h.Interval); return d }
|
||||
func (h *Health) Within() time.Duration { d, _ := time.ParseDuration(h.Timeout); return d }
|
||||
func (h *Health) GraceOf() time.Duration { d, _ := time.ParseDuration(h.Grace); return d }
|
||||
|
||||
// RunByRuntime says the container runtime runs this check as the container's own: exec and runtime.
|
||||
func (h *Health) RunByRuntime() bool {
|
||||
return h != nil && (h.Kind == HealthExec || h.Kind == HealthRuntime)
|
||||
}
|
||||
|
||||
// Words is the check in a few words, as a verdict is said: "http /healthz on web".
|
||||
func (h *Health) Words() string {
|
||||
switch h.Kind {
|
||||
case HealthHTTP:
|
||||
return "http " + h.Path + " on " + orPort(h.Endpoint, h.Port)
|
||||
case HealthTCP:
|
||||
return "tcp on " + orPort(h.Endpoint, h.Port)
|
||||
case HealthTool:
|
||||
return "its tool " + h.Tool
|
||||
case HealthRuntime:
|
||||
return "its image's own check"
|
||||
case HealthExec:
|
||||
return "its command"
|
||||
case HealthUnit:
|
||||
return "its unit"
|
||||
}
|
||||
return h.Kind
|
||||
}
|
||||
|
||||
func orPort(endpoint string, port int) string {
|
||||
if endpoint != "" {
|
||||
return endpoint
|
||||
}
|
||||
return fmt.Sprint(port)
|
||||
}
|
||||
|
||||
// problems holds a resource's health to its kind and bounds. container says whether the resource is a
|
||||
// container; longRunning whether it stays up.
|
||||
func (h *Health) problems(where string, container, longRunning bool) []string {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, where+": "+fmt.Sprintf(format, args...))
|
||||
}
|
||||
if !longRunning {
|
||||
say("health is judged on what stays up; a step or a scheduled run is judged by its own outcome")
|
||||
}
|
||||
switch h.Kind {
|
||||
case HealthHTTP, HealthTCP:
|
||||
if h.Port < 1 || h.Port > 65535 {
|
||||
say("a %s check needs the port it looks at", h.Kind)
|
||||
}
|
||||
case HealthExec:
|
||||
if !container {
|
||||
say("an exec check runs inside a container")
|
||||
}
|
||||
if strings.TrimSpace(h.Command) == "" {
|
||||
say("an exec check needs a command")
|
||||
}
|
||||
case HealthRuntime:
|
||||
if !container {
|
||||
say("a runtime check is a container image's own")
|
||||
}
|
||||
case HealthUnit:
|
||||
if container {
|
||||
say("a unit check is a service's or a process's own")
|
||||
}
|
||||
case HealthTool:
|
||||
if strings.TrimSpace(h.Tool) == "" {
|
||||
say("a tool check names the tool")
|
||||
}
|
||||
default:
|
||||
say("health of kind %q; it is runtime, http, tcp, exec, unit or tool", h.Kind)
|
||||
}
|
||||
if h.Kind == HealthHTTP {
|
||||
if !strings.HasPrefix(h.Path, "/") {
|
||||
say("an http check asks a path starting with /")
|
||||
}
|
||||
if h.Status != 0 && (h.Status < 100 || h.Status > 599) {
|
||||
say("an http check expects status %d, which is not one", h.Status)
|
||||
}
|
||||
if h.Scheme != "" && h.Scheme != "http" && h.Scheme != "https" {
|
||||
say("an http check is over http or https, not %q", h.Scheme)
|
||||
}
|
||||
}
|
||||
if strings.ContainsAny(h.Command, "\n\r") {
|
||||
say("an exec check's command is one line")
|
||||
}
|
||||
every, everyErr := time.ParseDuration(h.Interval)
|
||||
within, withinErr := time.ParseDuration(h.Timeout)
|
||||
grace, graceErr := time.ParseDuration(h.Grace)
|
||||
switch {
|
||||
case everyErr != nil || withinErr != nil || graceErr != nil:
|
||||
say("health's interval, timeout and grace are durations")
|
||||
default:
|
||||
if every < HealthIntervalFloor {
|
||||
say("a check looks no more often than every %s, not every %s", HealthIntervalFloor, every)
|
||||
}
|
||||
if within <= 0 || within >= every {
|
||||
say("a look takes more than nothing and less than its interval")
|
||||
}
|
||||
if grace < 0 {
|
||||
say("a grace is not negative")
|
||||
}
|
||||
if h.Looks >= HealthLooksFloor && grace+time.Duration(h.Looks)*every > HealthWithin {
|
||||
say("a grace and the failing looks take at most %s", HealthWithin)
|
||||
}
|
||||
}
|
||||
if h.Looks < HealthLooksFloor {
|
||||
say("a check is unhealthy after at least %d failing looks, not %d", HealthLooksFloor, h.Looks)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
// Package validate is the node-engine's own judgement of a declaration, for whoever must know before
|
||||
// a declaration is sent whether a machine would take it (novox/hq to-be 45 §4, D1; §9, the merge gate).
|
||||
//
|
||||
// **One validator.** The controller composed declarations the host then refused whole — a manifest
|
||||
// the catalogue check passed (novox/hq issue 236), a consumer's identity too long for the machine's
|
||||
// names (issue 263) — because the only validator was the one the host runs as it applies. A second,
|
||||
// written in the controller from the host's rules, would drift from them the first time either
|
||||
// changed. So the host's own parsing is exported here, unchanged: what the controller's self-check and
|
||||
// the merge gate run is what every machine runs.
|
||||
//
|
||||
// It judges a declaration as it arrives over the link: actions are refused, as the host refuses them
|
||||
// from the link (novox/hq ADR 0005). Nothing here touches a machine.
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// Declaration is every problem the node-engine would refuse a declaration for, each in its own words;
|
||||
// nil when it would take it. The body is the declaration as the controller composes it — the bytes a
|
||||
// signature is made over — not the signed envelope.
|
||||
func Declaration(raw []byte) []string {
|
||||
_, err := declaration.Parse(raw)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var refused *declaration.RefusalError
|
||||
if errors.As(err, &refused) {
|
||||
return refused.Problems
|
||||
}
|
||||
return []string{err.Error()}
|
||||
}
|
||||
|
||||
// Version is the declaration vocabulary this validator speaks: a declaration of another version is
|
||||
// refused whole by Declaration, as by the host.
|
||||
const Version = declaration.Version
|
||||
Reference in New Issue
Block a user