mesh/delivery-group group feat/sdk1 rejected: composed together: the checker's verdict at the members' heads (428d048dd50a): unit: git.novox.be/novox/mesh-sdk/…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-sdk/go/process.TestStatePutWritesAKeyWhole: a Put through a short blackout: 0, mesh-mail's state send…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
484 lines
26 KiB
Go
484 lines
26 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SettingsBy is the layers that apply to each module, keyed by module name.
|
|
type SettingsBy map[string][]Layer
|
|
|
|
// Generator works out a module's resources for one node, where they cannot be written in advance.
|
|
type Generator interface {
|
|
// Resources for this node. Absent means the node is not part of whatever this generates,
|
|
// which is an ordinary answer rather than a failure — a machine assigned the module before it
|
|
// has an address on the network is in exactly that state.
|
|
Resources(node string) ([]map[string]any, bool, error)
|
|
}
|
|
|
|
// OpensPorts is a generator that also says what its resources accept connections on.
|
|
//
|
|
// **Separate from Generator, because most generators have nothing to say here** and requiring an
|
|
// empty method of each would be a cost paid everywhere for one caller.
|
|
//
|
|
// It exists because a static field cannot express this. A hub accepts connections from every node
|
|
// at other sites; a machine that is not a hub dials out and needs nothing open — and they are the
|
|
// same module. `listens` in a manifest is one answer for every machine that runs it, so the
|
|
// machine that most needs filtering, the one facing the public internet, was the one whose rule
|
|
// set would have closed its own overlay.
|
|
type OpensPorts interface {
|
|
// Listens is what this node accepts on because of what was computed for it. Nothing is the
|
|
// ordinary answer: most machines running a computed module open no port at all.
|
|
Listens(node string) ([]Listening, error)
|
|
}
|
|
|
|
// Grant is one consumer's credential, on the machine that must create it.
|
|
type Grant struct {
|
|
// Provision is what was required.
|
|
Provision string
|
|
// Consumer is the node that will use it.
|
|
Consumer string
|
|
// From is the module on that machine which asked.
|
|
//
|
|
// **Part of who this credential is for, not a label** (novox/hq 04-ISSUES/022). Together with
|
|
// Consumer it names one consumer; the node alone does not, because a machine routinely runs
|
|
// several modules wanting the same thing. It is also what the provider names the role or the
|
|
// bucket or the client after, so withdrawing one consumer does not take another's away.
|
|
From string
|
|
// Values are what that module contributed — the name it wants, and anything else the
|
|
// provision's own vocabulary defines.
|
|
Values map[string]any
|
|
// Local is the name the credential goes by inside the consumer where it keeps several for one
|
|
// provision (ADR 0094); empty for the ordinary one. The provider sees it as a holder of its own.
|
|
Local string
|
|
// Slug is the consumer module's identity slug, if it declared one — carried on the grant so the
|
|
// provider side derives the same login the consumer does, even across nodes where the consumer's
|
|
// manifest is not in view (novox/hq ADR 0049). Empty means "use the module name".
|
|
Slug string
|
|
// At is where the consuming machine is on the private network, empty if it is not on one.
|
|
//
|
|
// Passed in with the grant because it is a fact about another machine, and resolution answers
|
|
// questions about one. A provider that must reach back to its consumer — a reverse proxy is
|
|
// the whole reason this exists — otherwise has to know how the mesh names machines.
|
|
At string
|
|
// Sealed is the credential, closed to the providing node.
|
|
Sealed string
|
|
}
|
|
|
|
// Rendering is everything needed to turn a resolution into the declaration a node is sent.
|
|
type Rendering struct {
|
|
// Certificate is what the mesh issued for this machine's internal name, and the mesh's own
|
|
// certificate. Both public — the key they belong to never left the machine.
|
|
Certificate string
|
|
Authority string
|
|
|
|
// Needed is each module's own secrets, sealed to this node, keyed by module and then by the
|
|
// name the module gave it.
|
|
Needed map[string]map[string]string
|
|
// Foreseen is each own secret not made yet that the next send WILL make, keyed like Needed: a
|
|
// composition asked ahead of the send (the self-check's D1) composes it with ForeseenSealed in
|
|
// its place and lists it in Composed.Foreseen, rather than failing for a value only a send
|
|
// makes. Nil on every composition that is sent, and on every other question, which then refuse
|
|
// a secret not made with a *NotMadeError.
|
|
Foreseen map[string]map[string]bool
|
|
// VaultSealed is, for a module on the core's list (CoreCredentialModules), each own secret as the vault sealed it
|
|
// to this machine (the mesh's sealed box, as the vault answered it), keyed like Needed (novox/hq ADR 0349 §16,
|
|
// issue 755). Given only for a machine whose engine opens the box (VaultBoxContract): its file then carries the
|
|
// box and never the controller's own sealed value. Nil everywhere else, and the composition is as it was.
|
|
VaultSealed map[string]map[string]json.RawMessage
|
|
// NotPlaced is each core module left out of this machine's declaration because the vault gave nothing for its
|
|
// credential and no box was kept, with why (issue 755): the machine keeps what it holds of it, and a module not
|
|
// yet there is not placed. Nil everywhere else.
|
|
NotPlaced map[string]string
|
|
|
|
// Mesh is every node's address on the private network, which is what a rule saying "from the
|
|
// mesh" resolves to. Passed in for the same reason grants are: who else is on the network is
|
|
// a fact about the mesh, and resolution answers questions about one machine.
|
|
Mesh []string
|
|
// Suffix is what a machine's internal name ends in, as the control plane composed Names —
|
|
// `internal` unless the operator chose another — so a fact writing those names does not
|
|
// compose it a second time.
|
|
Suffix string
|
|
|
|
// BusUsers is the mesh's composed user list, for the module holding `mesh-broker`. Empty on
|
|
// every other node, and on this one until the controller has composed it.
|
|
//
|
|
// **Only the users, never the server's own settings**: those are the module's, in its image and
|
|
// its mounts (Manifest.BusUsers).
|
|
BusUsers string
|
|
// BusMembership is this machine's membership for the bus the mesh is moving to, sealed to it
|
|
// (design 28, task 5.2). Empty for a machine not being moved. Written as a file the host reads
|
|
// after the declaration has applied, so the bus it names is standing before the machine leaves
|
|
// the one it is on.
|
|
BusMembership string
|
|
|
|
// MeshRange is the private network's CIDR (the range node addresses are allocated from), for a
|
|
// module that must name the whole mesh rather than one machine — an intrusion filter that must
|
|
// never ban a tunnel peer, say. A per-mesh value the module cannot know, so it is carried here
|
|
// and offered as ${machine:mesh-range}, the same way one machine's address is.
|
|
MeshRange string
|
|
|
|
// Accounts is each machine's operator account, by the same internal name Names uses (novox/hq
|
|
// to-be 29). What an ssh Host block's `User` line is composed from; empty for a machine no
|
|
// operator account is known on.
|
|
Accounts map[string]string
|
|
|
|
// Kept is every operator-sealed secret in the mesh, for a module that `keeps` them. Nil when
|
|
// nothing on this node keeps them, or the mesh has no operator key.
|
|
Kept *KeptExport
|
|
// KeptRuns is, by module, the file of the export as the node runs it, for a composer that holds no export (a node
|
|
// composing its own: the export is the controller's store's, issue 1126, C6): carried as it runs when Kept is nil.
|
|
KeptRuns map[string]map[string]any
|
|
|
|
// OutwardLinks is the links this machine reported as facing outside it, which the filter is
|
|
// written around (novox/hq ADR 0140). Empty means the machine has not said, and the mesh
|
|
// composes no filter for it rather than writing a rule around a link with no name.
|
|
OutwardLinks []string
|
|
|
|
// LocalNetworks is this machine's local networks (novox/hq ADR 0340): the networks it reported on a
|
|
// link facing outside it that lie wholly in a private range (JudgeLocalNetworks), which a port
|
|
// reached from `local-network` is admitted from beside the private network. Empty when it reported
|
|
// none, and the rule set then says so rather than widening the port or dropping it.
|
|
LocalNetworks []string
|
|
|
|
// TunnelInterface is the interface the mesh's private network runs on, named here rather than
|
|
// imported because the overlay package rests on this one. Traffic arriving on it is the mesh's,
|
|
// not this machine's own guest, so the filter admits it only by a rule.
|
|
TunnelInterface string
|
|
|
|
// Foundation is the ports the mesh itself needs reachable on every machine, which no module
|
|
// declares because the foundation is not a module (novox/hq 04-ISSUES/051 and 052). The broker
|
|
// is the one that matters: a machine dials it to enrol, and a firewall derived only from
|
|
// modules closes it.
|
|
Foundation []int
|
|
|
|
// Names is every machine's internal name and its address, for containers to be given.
|
|
//
|
|
// **A container does not inherit the machine's names**, so every internal name the mesh wrote
|
|
// is invisible to what the machine runs. Given here rather than looked up on the machine,
|
|
// because which machines exist is a fact about the mesh.
|
|
Names map[string]string
|
|
|
|
// Machines is only the machines, by the same internal name — the subset of Names that is a
|
|
// node of this mesh rather than a name it was told to serve. Both matter and they are not the
|
|
// same set: a container's hosts wants every name, so a routed name resolves to the proxy that
|
|
// serves it, while a resolver told the mesh's suffix is authoritative for it answers from what
|
|
// it is given and forwards nothing — so a routed name written there is a name nobody asks for,
|
|
// standing beside the machines and looking as real as they do.
|
|
Machines map[string]string
|
|
|
|
// Zones is every zone a module in the mesh answers itself, where it is answered (novox/hq ADR
|
|
// 0199): the mesh's resolver forwards each one there.
|
|
Zones []ZoneAt
|
|
|
|
// Holders is, for each replicated mesh seat, every machine holding it, by internal name and
|
|
// private address (novox/hq ADR 0223) — the same shape as Machines. What a machine's resolver
|
|
// file lists: every holder of the mesh's resolver, this machine first if it is one.
|
|
Holders map[string]map[string]string
|
|
|
|
Settings SettingsBy
|
|
Generators map[string]Generator
|
|
// Grants are the credentials this node must create, for the provisions it offers. Passed in
|
|
// rather than resolved, because who consumes a node is a fact about the rest of the mesh and
|
|
// resolution answers questions about one machine.
|
|
Grants []Grant
|
|
// Withheld is every consumer left out of Grants because its identity overflows the provision's
|
|
// bound (novox/hq ADR 0225). Composed into nothing; carried so the machine's declaration can say
|
|
// whom it does not serve, and why, beside what it does.
|
|
Withheld []Overflow
|
|
// Unbound is every consumer whose pair credential from this node is on record and whose own
|
|
// resolution binds it elsewhere (novox/hq issue 274): never granted, carried to be said.
|
|
Unbound []Unbound
|
|
|
|
// Ports is where this machine puts what each module needs reachable, by module and by the
|
|
// port the software itself uses (novox/hq ADR 0038).
|
|
//
|
|
// **The one place the number now lives.** A module used to write it three times — for the rule
|
|
// set, for what a consumer is told, and for what the runtime publishes — and nothing checked
|
|
// that the three agreed. They are all derived from this.
|
|
Ports map[string]map[int]int
|
|
|
|
// Adopted says the node is adopted (novox/hq ADR 0100): the firewall found on it stays in
|
|
// force, so no module that loads a filter is declared there, and what the mesh needs
|
|
// reachable is declared as openings, with its own ports guarded by a table that only refuses.
|
|
Adopted bool
|
|
|
|
// Given is the machine ports this node was given for its modules' ports, by module and by the
|
|
// port the software uses (novox/hq ADR 0100) — the foundation's ports, as genesis chose them.
|
|
// They win over anything the mesh would assign and over a manifest's own long-form mapping.
|
|
Given map[string]map[int]int
|
|
|
|
// Planned says this rendering composes the plans the seat mesh-provisioner keeps (compose.Planning). A contribution a
|
|
// plan carries is then refused where its port is none the module listens on: no machine port stands for it, and its
|
|
// provider dials what the plan says (issue 774, research 072 row 34).
|
|
Planned bool
|
|
|
|
// Taken is the modules taken on this adopted node (novox/hq ADR 0100). The guard is derived
|
|
// from these only (ADR 0103): a port of a module assigned but not taken may still be the
|
|
// predecessor's.
|
|
Taken map[string]bool
|
|
|
|
// Seats is where this machine put each mesh-scoped seat's holder, by seat and by the port the
|
|
// holder's software uses (novox/hq 04-ISSUES/102) — read from the node's settings and
|
|
// assignments for whichever module claims the seat, whether or not it is in this node's set.
|
|
// What ${seat:…} answers with; see seat_into.go for why the answer may be absent.
|
|
Seats map[string]map[int]int
|
|
|
|
// ArtifactStore is the mesh's artifact store as this network reaches it (host:port) — the
|
|
// node holding it and the port that node put it on — or empty when the mesh has none on its
|
|
// network yet. Composed into every image and archive the mesh built, at this moment and never
|
|
// stored (novox/hq 04-ISSUES/102).
|
|
ArtifactStore string
|
|
|
|
// SeatReach is where this machine reaches the holder of each mesh-scoped seat it may be asked
|
|
// about (host:port), by seat: what ${seat:<seat>:reach} answers with (novox/hq ADR 0222). Absent
|
|
// when no holder is reachable yet; see seat_into.go for which seats are answered.
|
|
SeatReach map[string]string
|
|
|
|
// Built is every `<module>/<artifact>` the mesh has built. What tells a reference recorded
|
|
// with an address — before references were kept without one — from an image a module runs
|
|
// straight from a public registry.
|
|
Built map[string]bool
|
|
|
|
// DataRoot is where this node keeps the directories the mesh places for its modules
|
|
// (novox/hq ADR 0112, to-be 27): a directory resource that states no path resolves to
|
|
// <DataRoot>/<module>/<id>, and ${dir:<id>} names that place from the module's own files,
|
|
// mounts and environment. A node setting fixed at installation; empty means the default,
|
|
// /var/lib — see dir_into.go.
|
|
DataRoot string
|
|
|
|
// ReadsHealth says this machine's node-engine reads a resource's `health` (novox/hq ADR 0240 Phase B:
|
|
// its statement's contract is link.ReadinessContract or later). An older engine parses strictly and
|
|
// refuses a field it does not know, whole — so to it the field is not sent, and what it runs is
|
|
// judged by liveness alone.
|
|
ReadsHealth bool
|
|
|
|
// JudgesRoot says this machine's node-engine judges a user's declared `root` (novox/hq ADR 0266: its
|
|
// statement's contract is link.RootContract or later). To an older, strict engine the field is not
|
|
// sent, and the account it names is not judged — which the self-check says, as not judged.
|
|
JudgesRoot bool
|
|
|
|
// ReadsBusy says this machine's node-engine reads a container's `busy` (novox/hq ADR 0335: its statement's
|
|
// contract is link.BusyContract or later). To an older, strict engine the field is not sent, and the
|
|
// container is replaced at once, as before.
|
|
ReadsBusy bool
|
|
|
|
// Hands says this machine's node-engine writes a filled file on the provisioner client's word alone (its
|
|
// statement's contract is link.HandedContract or later, design 57 P7), and Provisioner is the account the
|
|
// provisioner's client runs as here, empty where none runs (novox/hq ADR 0349 §4). With both, a moved module's
|
|
// templated files are handed to the client (handed.go); without either, it composes as before.
|
|
Hands bool
|
|
Provisioner string
|
|
// Assigned is, by module, whether the vault judged it handed (AssignmentHands of the plan in its assignment), as the
|
|
// vault answered the node composing itself; where it names a module, Handing follows it alone (issue 774). Nil, or a
|
|
// module it does not name, is judged by the manifest and the machine, as the composer of the plans does.
|
|
Assigned map[string]bool
|
|
}
|
|
|
|
// RootField is a user resource's field saying the account must never become root without a person
|
|
// (novox/hq ADR 0266).
|
|
const RootField = "root"
|
|
|
|
// Composed is a declaration's resources and which module each came from.
|
|
//
|
|
// Owner is kept beside the resources because a resource id cannot be split back into its module:
|
|
// a module's name may itself contain a dot. What the mesh adds of its own — an opening, the guard —
|
|
// has no owner.
|
|
//
|
|
// Received is what each module on the machine is given for each requirement it receives — the same
|
|
// contributions its received file is written from, kept beside it so the mesh can also issue them
|
|
// on the bus in the module's membership (novox/hq ADR 0167). By module, then requirement.
|
|
type Composed struct {
|
|
Resources []map[string]any
|
|
Owner map[string]string
|
|
Received map[string]map[string][]Contribution
|
|
// LeftOut is every module of this machine's set that was left out of its declaration, and
|
|
// why (novox/hq ADR 0163, rule 6): a setting stored for it that its definition can no longer
|
|
// compose. Its held things are kept and its containers untouched — the machine is told so —
|
|
// and it is told everything else.
|
|
LeftOut map[string]string
|
|
// Foreseen is every own secret composed with ForeseenSealed in its place (Rendering.Foreseen),
|
|
// as `module/name`, sorted: what the next send will make. Never set on a declaration that is
|
|
// sent — a placeholder in a sealed file is a credential the process cannot read.
|
|
Foreseen []string
|
|
// Unplaced is every contribution the holder's template could not render into something, each
|
|
// naming its module and why (novox/hq ADR 0255): that piece is left out and the machine is told
|
|
// everything else. Push and plan say it; the merge gate refuses a change that adds one.
|
|
Unplaced []string
|
|
// HandedOnly is each file left out because only the provisioner's client fills it and the module is not handed on
|
|
// this machine (a provider's consumers file, ADR 0373), said once in the plan. Not an unplaced contribution, which
|
|
// the merge gate refuses: a module not handed composes as it did.
|
|
HandedOnly []string
|
|
// Filled is the path of every file handed to the provisioner, in the order composed: the declaration's
|
|
// filled files (novox/hq ADR 0349 §4, §5). Plans is each handed module's plan on this machine, by module.
|
|
// Both empty where nothing is handed, and the declaration is then what it was.
|
|
Filled []string
|
|
Plans map[string]Plan
|
|
}
|
|
|
|
// ForeseenSealed is what stands for an own secret a send will make, in a composition asked ahead of
|
|
// the send. Not a sealed value: nothing composed with it may be sent.
|
|
const ForeseenSealed = "foreseen: made by the next send"
|
|
|
|
const BusMembershipPath = "/var/lib/mesh/membership-next.json"
|
|
|
|
// mapping reads a container's port mapping — `[address:]outer:inner[/protocol]`, the address
|
|
// possibly an IPv6 one in brackets — indexing from the end, so an address's own colons never
|
|
// shift the ports. Not ok for a short form or anything that is not a mapping.
|
|
func mapping(written string) (outer, inner int, address string, ok bool) {
|
|
written = strings.TrimSpace(written)
|
|
if cut := strings.LastIndex(written, "/"); cut >= 0 {
|
|
written = written[:cut]
|
|
}
|
|
parts := strings.Split(written, ":")
|
|
if len(parts) < 2 {
|
|
return 0, 0, "", false
|
|
}
|
|
inner, err := strconv.Atoi(parts[len(parts)-1])
|
|
if err != nil {
|
|
return 0, 0, "", false
|
|
}
|
|
outer, err = strconv.Atoi(parts[len(parts)-2])
|
|
if err != nil {
|
|
return 0, 0, "", false
|
|
}
|
|
return outer, inner, strings.Join(parts[:len(parts)-2], ":"), true
|
|
}
|
|
|
|
// Contribution is one module telling the answer to a requirement what it needs from it.
|
|
type Contribution struct {
|
|
// From is the module that said it, so the provider and a person reading the file can tell
|
|
// which route belongs to what.
|
|
From string `json:"from"`
|
|
// Node is the machine it said it from, empty when that is this one.
|
|
//
|
|
// A provision answered from anywhere in the mesh has consumers on other machines, and the
|
|
// provider has to know who they are — a database told to create a password and not who for
|
|
// cannot do anything with it. Contributions were node-local until this, which meant the one
|
|
// case that most needed them was the one they did not reach.
|
|
Node string `json:"node,omitempty"`
|
|
// At is where that machine is on the private network, empty when it is not on one or when it
|
|
// is this machine.
|
|
//
|
|
// The mesh knows it and a provider should not have to derive it. A reverse proxy is told
|
|
// *send traffic to this consumer* and has to open a connection — so without this every
|
|
// provider that reaches back to a consumer would have to know how the mesh names machines,
|
|
// which is a convention leaking into every module that implements a provision.
|
|
At string `json:"at,omitempty"`
|
|
// As is who this consumer is: what the provider should call the login it creates.
|
|
//
|
|
// **Said by the mesh rather than invented by the provisioner** (novox/hq 04-ISSUES/023). It
|
|
// used to be neither — the provisioner made a name, and the consumer, which has to present it
|
|
// to authenticate, had no way to learn it. One derivation reaches both ends, so they agree by
|
|
// construction.
|
|
// Omitted when there is none. A contribution that is not a credential grant — a module
|
|
// offering something to another on its own machine — has nobody to be identified to, and a
|
|
// field that is always present and usually empty teaches a reader to ignore it.
|
|
As string `json:"as,omitempty"`
|
|
// Secret is the file on this machine holding that consumer's credential, sealed to it.
|
|
//
|
|
// Named rather than carried, for the same reason the private network's key is: the mesh
|
|
// discarded the value and could not put it here if it wanted to. What is here is where to
|
|
// find it.
|
|
Secret string `json:"secret,omitempty"`
|
|
// Values are the module's own, with settings applied. What the keys mean is agreed by the
|
|
// requirement's name — everything providing `reverse-proxy` understands the same shape, which
|
|
// is what makes swapping one for another cost nothing.
|
|
Values map[string]any `json:"values"`
|
|
// Derived is what this provider's own definition said it derives for this consumer, already
|
|
// derived (novox/hq ADR 0202).
|
|
//
|
|
// **The provider is told, rather than recomputing it.** A served value may name the consumer's
|
|
// identity — a bucket named for who is asking, a database prefixed with it — and before this
|
|
// the rule lived twice: once in the provisioner's code, once transcribed into every consumer's
|
|
// definition. The mesh fills the provider's own statement here and delivers the same filled
|
|
// value to the consumer, so the two cannot disagree: there is no second computation to
|
|
// disagree with.
|
|
//
|
|
// Only the keys that are per-consumer. The rest of what the provider serves is the same for
|
|
// everyone and is in its own definition, where it already is.
|
|
Derived map[string]any `json:"derived,omitempty"`
|
|
}
|
|
|
|
// Kept is one secret as the operator can recover it: where it belongs, and the value sealed to the
|
|
// operator's key. Never a node's blob, and never a value.
|
|
//
|
|
// Two kinds, addressed the same way — by the node and module that hold it and the name they know
|
|
// it by. An `own` secret is one a module has for itself; a `pair` secret is a credential the
|
|
// module was granted for a provision it requires (`name` is the provision), and Provider says which
|
|
// node grants it.
|
|
type Kept struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Name string `json:"name"`
|
|
// Kind is `own` or `pair`.
|
|
Kind string `json:"kind"`
|
|
// Provider is the node granting a pair credential; empty for an own secret.
|
|
Provider string `json:"provider,omitempty"`
|
|
// Origin is `made` or `accepted` — whether the mesh minted it or a person supplied it.
|
|
Origin string `json:"origin"`
|
|
// Sealed is the value, sealed to the operator key named in Key.
|
|
Sealed string `json:"sealed"`
|
|
Key string `json:"key"`
|
|
MadeAt time.Time `json:"made-at"`
|
|
// Local is the credential's name inside the consumer where it holds several (ADR 0094).
|
|
Local string `json:"local,omitempty"`
|
|
}
|
|
|
|
// KeptExport is what a person keeps beside the operator key, and what a vault keeps on its disk:
|
|
// every operator-sealed copy, and the honest list of what has none.
|
|
type KeptExport struct {
|
|
Export int `json:"export"`
|
|
OperatorKey string `json:"operator-key"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
// Kept is sealed to OperatorKey. EarlierKey is sealed to a key the mesh has since replaced —
|
|
// recoverable with that key, if the person still has it, and with nothing else. Unrecoverable
|
|
// has no operator copy at all.
|
|
Kept []Kept `json:"kept"`
|
|
EarlierKey []Kept `json:"sealed-to-earlier-key,omitempty"`
|
|
Unrecoverable []Kept `json:"unrecoverable,omitempty"`
|
|
}
|
|
|
|
// sortedKeys is map iteration made repeatable, which everything written to a machine needs.
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func asPort(v any) (int, bool) {
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n, true
|
|
case float64:
|
|
return int(n), true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// PreparationArgument is how the mesh asks a module to prepare its state: one word, to the module's
|
|
// own program, whatever that program is (novox/hq ADR 0135).
|
|
//
|
|
// **One word for every kind of module.** A module built as a Go binary receives it as its argument;
|
|
// one built as a bundle receives it through the runtime, whose entry takes the same word. So the
|
|
// mesh has one way of asking and a module has one way of answering, and neither learns the other's
|
|
// shape.
|
|
const PreparationArgument = "prepare"
|
|
|
|
// endpointPorts is a module's endpoint names against the ports they listen on.
|
|
func endpointPorts(m Manifest) map[string]int {
|
|
out := map[string]int{}
|
|
for _, l := range m.Listens {
|
|
if name := strings.TrimSpace(l.Name); name != "" {
|
|
out[name] = l.Port
|
|
}
|
|
}
|
|
return out
|
|
}
|