Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
142 lines
6.2 KiB
Go
142 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// sendable is a declaration as a machine is sent it: its resources and, for an adopted node, its
|
|
// mode and which modules were taken on it (novox/hq ADR 0100).
|
|
//
|
|
// **Body is the only place the envelope is marshalled.** It was written by hand at every send site,
|
|
// in the digest the mesh compares, and in `plan --json`; a key added at one and not another would
|
|
// make a machine look out of date for ever, or send something `plan` never showed.
|
|
type sendable struct {
|
|
Resources []map[string]any
|
|
// Sequence orders this send against every other to the same node: one higher each time, taken
|
|
// under the node's hold just before the body is made (novox/hq 04-ISSUES/107). Zero is not sent
|
|
// at all, which a host reads as "no order claimed" — the shape of every declaration before this.
|
|
Sequence int64
|
|
// Epoch is the controller lease's epoch it was composed under (novox/hq to-be 45 §6): a machine that
|
|
// heard a later epoch refuses it. Zero is not sent at all — every machine whose node-engine has not
|
|
// said it reads one is sent none, because an older node-engine refuses a key it does not know, whole
|
|
// (link/order.go, the contract).
|
|
Epoch uint64
|
|
// Adoption is nil for a converged node, and then the body is byte for byte what it was before
|
|
// adoption existed: an older host parses the envelope strictly and would refuse the key.
|
|
Adoption *adoptionEnvelope
|
|
|
|
// Received and Mesh are not sent in the declaration. They are what this machine's memberships
|
|
// are issued with on the bus (novox/hq ADR 0167): each module's received contributions, from
|
|
// the same composition as its received files, and every machine's private-network address.
|
|
Received map[string]map[string][]catalogue.Contribution
|
|
Mesh []string
|
|
// BusUsers is the bus's user list this declaration carries, empty for every machine but the one
|
|
// holding the bus; not sent apart from the file it is in. Its digest is recorded once sent, so
|
|
// whether that machine must go first is read from the list alone (novox/hq issue 249).
|
|
BusUsers string
|
|
// LeftOut is every module of the machine's set left out of this declaration because a stored
|
|
// setting cannot compose with its definition (novox/hq ADR 0163, rule 6), sorted. The host
|
|
// keeps that module's held things and touches none of its containers; a machine is told
|
|
// everything or nothing about what it IS told, and what it is not told is said. Absent from
|
|
// the body when empty, so a declaration that leaves nothing out is byte for byte what it was.
|
|
LeftOut []string
|
|
// leftOutWhy is why each was, for push and plan to say; never on the wire.
|
|
leftOutWhy map[string]string
|
|
// withheld is every consumer this machine's grants leave out, because its identity overflows the
|
|
// provision's bound (novox/hq ADR 0225); for push and plan to say, never on the wire.
|
|
withheld []catalogue.Overflow
|
|
// Builds is the build of each module this declaration carries — module to the commit its build
|
|
// was made from — recorded with the send and never on the wire (novox/hq issue 259, ADR 0221).
|
|
// Composed only on the send path; nil records that it is not known.
|
|
Builds map[string]string
|
|
}
|
|
|
|
// adoptionEnvelope 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). Ids
|
|
// rather than a rule to split them by, because a module's name may contain a dot.
|
|
type adoptionEnvelope struct {
|
|
Taken []string `json:"taken"`
|
|
Untaken map[string][]string `json:"untaken,omitempty"`
|
|
}
|
|
|
|
// Body is the declaration's bytes, as sent and as digested.
|
|
func (s sendable) Body() ([]byte, error) {
|
|
envelope := map[string]any{"declaration": 1, "resources": s.Resources}
|
|
if s.Adoption != nil {
|
|
envelope["adoption"] = s.Adoption
|
|
}
|
|
if s.Sequence > 0 {
|
|
envelope["sequence"] = s.Sequence
|
|
}
|
|
if s.Epoch > 0 {
|
|
envelope["epoch"] = s.Epoch
|
|
}
|
|
if len(s.LeftOut) > 0 {
|
|
envelope["left_out"] = s.LeftOut
|
|
}
|
|
// An empty declaration is deliberate here — 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, so a truncated or mis-composed body is never mistaken for "own nothing".
|
|
if len(s.Resources) == 0 {
|
|
envelope["owns_nothing"] = true
|
|
}
|
|
return json.Marshal(envelope)
|
|
}
|
|
|
|
// adoptionOf is the envelope for a node, nil when it is converged.
|
|
func adoptionOf(ctx context.Context, inv *inventory.Inventory, record inventory.Node,
|
|
plan catalogue.Resolution, composed catalogue.Composed) (*adoptionEnvelope, error) {
|
|
if !record.Adopted {
|
|
return nil, nil
|
|
}
|
|
taken, err := inv.Taken(ctx, record.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return adoptionFor(plan, taken, composed), nil
|
|
}
|
|
|
|
// adoptionFor is the envelope computed from what was taken and who owns each resource.
|
|
//
|
|
// Every module the node runs that is not taken is untaken — including one pulled in by another
|
|
// rather than assigned: what is found is kept until its module is taken, whoever put it there.
|
|
func adoptionFor(plan catalogue.Resolution, taken []string,
|
|
composed catalogue.Composed) *adoptionEnvelope {
|
|
isTaken := map[string]bool{}
|
|
for _, m := range taken {
|
|
isTaken[m] = true
|
|
}
|
|
out := &adoptionEnvelope{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): a directory, a service, a
|
|
// container mounting what was found and an action run in a held container all reach what
|
|
// the machine already has. 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
|
|
}
|