Files
mesh-sdk/go/catalogue/declaration.go
T
jschoubben 1c7356c247
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…
catalogue: the composition's rules and refusals leave for the node-engine, so mesh-sdk renders no declaration and refuses no composition (ADR 0593 §7)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:19:25 +01:00

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
}