package main import ( "encoding/json" "fmt" "git.novox.be/novox/mesh-sdk/go/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 // Generation is the assignment generation it was composed from (novox/hq issue 234): a machine that // applied a later one refuses it, so a send composed from a view of the assignments the mesh has moved // past cannot undeclare what is still assigned. Zero is not sent at all — every machine whose // node-engine has not said it reads one is sent none, for the reason the epoch is not (an older // node-engine refuses a key it does not know, whole). Generation int64 // composedFrom is the generation read before this declaration was composed, and actingEpoch the // lease epoch its sender acted under — whether or not the machine is sent either — for the record of // the send; never on the wire. composedFrom int64 actingEpoch uint64 // 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, because an older node-engine refuses a key it does not know, whole. ReadsModules bool // moduleOf is the composer's owner of each resource id, and buildIDs the build each module carries (its id): // kept beside the resources, and on the wire only to a machine that reads them. moduleOf map[string]string buildIDs map[string]string // modules are the modules this declaration names, for the record of the send; never on the wire. modules []string // localNetwork is every port admitted from the local network and what it resolved to (novox/hq ADR // 0340 §8), for plan to say; never on the wire. localNetwork []catalogue.LocalNetworkPort // 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 // unbound is every consumer whose credential from this machine is on record and that is bound // elsewhere (novox/hq issue 274); for push and plan to say, never on the wire. unbound []catalogue.Unbound // foreseen is every own secret composed with a stand-in, as `module/name`: what the next send will // make (Foreseeing, novox/hq issue 275). Never on the wire, and a declaration that has any is never // sent. foreseen []string // unplaced is every contribution its holder's template could not render, naming its module and // why (novox/hq ADR 0255): left out of this declaration, for push and plan to say, never on the wire. unplaced []string // handedOnly is each file left out because only the provisioner's client fills it and the module is not handed // here (a provider's consumers file, ADR 0373). handedOnly []string // 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 // Bindings is where each consumer of a provision that keeps its data is bound in this declaration // (novox/hq ADR 0232), recorded with the send and never on the wire. Composed only on the send path. Bindings []inventory.Binding // Filled is the path of every file this declaration hands to the provisioner, and Provisioner the account the // client that fills them runs as (novox/hq ADR 0349 §4, §5, design 57 P6): on the wire as `filled_files` and // `provisioner_account`, both absent when nothing is handed, so every other declaration is byte for byte what // it was. plans is each handed module's plan, kept with the send for the assignments mirror; never on the wire. Filled []string Provisioner string plans map[string]catalogue.Plan // requiredBy is, for each handed module only pulled in by another, the module that requires it (issue 765): kept // beside its plan, so the assignments mirror gives the plan a value of its own. Never on the wire. requiredBy 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.ReadsModules { // The module of each resource the composer owns (what the mesh adds of its own has none), 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(s.Resources)) for n, r := range s.Resources { module, owned := s.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(s.buildIDs) > 0 { envelope["builds"] = s.buildIDs } } 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 } if s.Generation > 0 { envelope["generation"] = s.Generation } if len(s.Filled) > 0 { envelope["filled_files"] = s.Filled envelope["provisioner_account"] = s.Provisioner } // 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) }