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()) } 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 }