// Package frombus fills mesh-sdk's composition input from what is on the bus, so a node agent, the composition check // and the grants part compose with one composer over the same inputs (novox/hq ADR 0547 §2, ADR 0549 §3; issue 1126). // // **One composer, three callers.** compose.Compositions is the composer; Input is how each caller fills its // compose.Input from the bus: the node agent for its own node (ADR 0547 §2), the assigner's composition check for every // node (ADR 0539 §6's check, ADR 0547 §4), and the grants part (ADR 0547 §5). Each reads the bus with its own credential // through a Reader, so this package holds no bus client, no store and no timer: what it is given is all it reads. // // **What it reads, and from whom.** // - which modules run on each node, with their pins, holdings, host ports, bindings and settings placement: the // indexer's state `placement`, one key per node (ADR 0549 §2); // - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and // accounts): the same placement key, Placement.Decided; // - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the // node's own have key, `.node` (HaveNode.Record); the tunnel found at a node's adoption and the names of the hub's peers are decided, // in its placement, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both; // - every module's manifest: the indexer's `modules.` and its manifests by digest; for each module placed on // the composing node, its built manifest instead: the builder's done key at the trunk's FP2, or at the fingerprint // the node's want names where it runs another build (Scope.KeptAt), resolved by the controller's own take-in rule // (takein.Manifest), a moved module's templates archive read in the artifact store (issue 1126); // - each module's config values on a node: mesh-config's resolved state, the composing node's slice alone unless the // caller composes every node (ADR 0547 §2, the prepare step's L4); of another node, only the keys its modules' // served values fill (catalogue.ServedSettings), which every consumer is told (issue 1126). // // **Own secrets and pair credentials** come from the vault sealed to the composing node's key (OwnSecrets, Credentials), // for that node alone: as a consumer and as a provider, exactly the keys compose.CredentialsNeeded names. **Not yet // read** (each a later subtask of issue 1126): licences (none is placed today) and core boxes. Without one // the composition stands in for it, and Compose refuses a composition that stands in for anything (compose.ErrStoodIn): // a node never applies a stand-in. No node is joining before ADR 0547's third milestone (ADR 0550 §6), so Joining is // always empty. package frombus import ( "context" "encoding/json" "fmt" "net/netip" "slices" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/overlay" "git.novox.be/novox/mesh-sdk/go/takein" ) // Reader is the bus as a caller reads it with its own credential. Each method reads one part's state; an error is a // read that failed, never an absent key (an absent key is an empty answer). type Reader interface { // Placements is every node's placement, by node (delivery.PlacementBucket). Placements(ctx context.Context) (map[string]delivery.Placement, error) // Records is every node's record, by node: HaveNode.Record of each node's own have key, `.node` of // delivery.HaveBucket (ADR 0555 §3). Records(ctx context.Context) (map[string]delivery.NodeRecord, error) // Modules is every module the indexer holds, by module (delivery.ModulesBucket). Modules(ctx context.Context) (map[string]delivery.Module, error) // Manifest is the raw manifest of a digest (delivery.ManifestsBucket). Manifest(ctx context.Context, digest string) ([]byte, error) // Values is one module's config values on one node as mesh-config resolved them (mesh-config's state `resolved`, // keys `..`), decoded; nil when it has none. Values(ctx context.Context, node, module string) (map[string]any, error) // OwnSecrets is the own secrets of the composing node's modules, each sealed to that node's key, as the vault answers // the node's engine (the vault's verb `own`, ADR 0550 §5); asked only for the node composing itself. OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecretRow, error) // Credentials is, for the composing node, each pair credential it reads as a consumer, held and sealed to its key, // and each it grants as a provider, with the copy sealed to it (the vault's `own` with the keys, ADR 0550 §5). A key // the vault holds no value for is left out; the composition then stands in for it, and Compose refuses it. Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) // Build is a module's build at a fingerprint, the builder's key (delivery.BuilderBuildsBucket, BuilderBuildKey); nil // when the builder holds none. The composing node composes each of its modules from its build (issue 1126). Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) // Archive is the archive at url, a kept reference routed to the artifact store as the composing node reaches it // (catalogue.Routed), checked against digest: a moved module's templates archive, read back for its texts as the // controller's take-in reads it (takein.Built). Archive(ctx context.Context, url, digest string) ([]byte, error) // Served is, of one module's config values on another node than the composing one (mesh-config's state `resolved`, // keys `..`), the keys named, decoded: the keys its served values and zone name fill // (catalogue.ServedSettings), which every consumer is told and so every node may read (issue 1126). Nil when none // is set. Served(ctx context.Context, node, module string, keys []string) (map[string]any, error) } // Mesh is what is true of the whole mesh and is no part's state on the bus yet: the private network's range and the // internal names' suffix as genesis was told them, and the mesh's authority certificate (public). Empty fields take // compose's defaults. type Mesh struct { OverlayCIDR string InternalSuffix string Authority string // Predefined is the list of predefined keys mesh-config published, which a manifest's values falling back to a // predefined key are judged against; nil reads each manifest as the indexer does (catalogue.ParseManifestForInputs). Predefined catalogue.PredefinedList } // Scope says who composes. Node is the node composing; only its config values are read unless Every is set, as the // composition check sets it to compose every node (of another node's modules, only the values their served values fill // are read, Reader.Served). KeptAt is, by module, the fingerprint the composing node's want names where it differs from // the indexer's trunk: that node composes the module from its build at that fingerprint, as it runs it. // // **Composed from its build** (issue 1126): each module placed on the composing node, and each in KeptAt, composes from // its built manifest (takein.Manifest over the builder's done key at the trunk's FP2 or the kept fingerprint), as the // controller composes from the manifest its take-in resolved: so a moved module is handed to the provisioner, a resource // naming an artifact names what was built, and a templated file has its text. A placed module the builder holds no done // build of at the trunk's FP2 composes from the indexer's manifest, as before. Every leaves the catalogue as the indexer // read it: the check of every node judges placement, and reads no archive. // // Placements, when set, stands in for the indexer's placement of the nodes it names: a node's candidate placement // (delivery.PlacementCandidate), so a node composes in shadow with a catalogue pull request's placement and compares // the result with what it runs (issue 1126); a nil value removes the node's placement. The rest is read as always. // // InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again // (compose.Node.InOwn) rather than cut out as modules no build is given for (issue 1126). // // Running is what the composing node runs; of it, its own composition (`.node`) is given to the composition // (compose.Node.RunsOwn), so a module folded there that names a build of the old form is left to the node, carried // as it runs (issue 1126). Nil composes every folded module from its manifest. type Scope struct { Node string Every bool KeptAt map[string]string Placements map[string]*delivery.Placement InOwn []string Running []delivery.Composed // SameValue, in an answer, says whether two sealed values open to the same value (the node opens them with its own // key), so a differing resource shows a value sealed again as such (delivery.CompareComposedWith). Nil shows them // as differing. SameValue func(a, b string) (same, known bool) } // InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs // (`.node` in running) carries, by the module their ids name (declaration.ModuleGuess), sorted: the modules the // controller's composed writer folded there because no fingerprint of the new form names them (mesh-wireguard, a module // run at a build of the old form; issue 1126). A node composing itself passes it as Scope.InOwn, so its own // composition keeps them as it runs them until a want names a build of the new form for one. func InOwn(running []delivery.Composed, p delivery.Placement) []string { placed := map[string]bool{} for _, m := range p.Modules { placed[m.Module] = true } in := map[string]bool{} for _, c := range running { if c.Module != "" { continue } for _, raw := range c.Resources { var head struct { ID string `json:"id"` Module string `json:"module"` } if json.Unmarshal(raw, &head) != nil || head.Module != "" { continue } if m, ok := declaration.ModuleGuess(head.ID); ok && placed[m] { in[m] = true } } } return sortedKeys(in) } // Input is the composition input for s, read through r. func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) { return input(ctx, r, mesh, s, nil) } // input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in is // handed to refuse and composed as a stub (compose.Stub, kept for that node alone) rather than ending the read, so a // node's answer names every module it cannot compose (issue 1126). func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error)) (compose.Input, error) { placements, err := r.Placements(ctx) if err != nil { return compose.Input{}, fmt.Errorf("frombus: placement: %w", err) } if len(s.Placements) > 0 { given := make(map[string]delivery.Placement, len(placements)+len(s.Placements)) for node, p := range placements { given[node] = p } for node, p := range s.Placements { if p == nil { delete(given, node) continue } if p.Node != node { return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node) } given[node] = *p } placements = given } records, err := r.Records(ctx) if err != nil { return compose.Input{}, fmt.Errorf("frombus: node records: %w", err) } modules, err := r.Modules(ctx) if err != nil { return compose.Input{}, fmt.Errorf("frombus: the indexer's modules: %w", err) } if _, ok := placements[s.Node]; !ok && !s.Every { return compose.Input{}, fmt.Errorf("frombus: %s has no placement in the catalogue", s.Node) } in := compose.Input{OverlayCIDR: mesh.OverlayCIDR, InternalSuffix: mesh.InternalSuffix, Authority: mesh.Authority, Catalogue: map[string]catalogue.Manifest{}} if in.Authority == "" { // The mesh's own certificate, public, as the composing node holds it; for the check of every node, as the first // node holding one does. if rec, ok := records[s.Node]; ok && rec.Authority != "" { in.Authority = rec.Authority } else { for _, node := range sortedKeys(records) { if a := records[node].Authority; a != "" { in.Authority = a break } } } } parse := func(raw []byte) (catalogue.Manifest, error) { if mesh.Predefined != nil { return catalogue.ParseManifestWith(raw, mesh.Predefined) } return catalogue.ParseManifestForInputs(raw) } read := func(module, digest string) (catalogue.Manifest, error) { raw, err := r.Manifest(ctx, digest) if err != nil { return catalogue.Manifest{}, fmt.Errorf("frombus: the manifest of %s (digest %s): %w", module, digest, err) } return parse(raw) } for _, name := range sortedKeys(modules) { m := modules[name] if m.Manifest == "" { continue // a registered module the indexer has not read: nothing to compose it from } man, err := read(name, m.Manifest) if err != nil { return compose.Input{}, err } in.Catalogue[name] = man } // The module the mesh provides itself (the private network) has no repository the indexer reads: its manifest comes // with mesh-sdk and always wins, as the controller's providedModules and the grants part have it. provided, err := json.Marshal(overlay.Manifest()) if err != nil { return compose.Input{}, err } var private catalogue.Manifest if err := json.Unmarshal(provided, &private); err != nil { return compose.Input{}, fmt.Errorf("frombus: the private network's manifest mesh-sdk provides: %w", err) } in.Catalogue[overlay.Name] = private var missing []string for _, node := range nodesOf(placements, records) { rec, hasRecord := records[node] p := placements[node] if !hasRecord { if len(p.Modules) > 0 { missing = append(missing, node) } continue } n := nodeOf(rec, p) withValues := s.Every || node == s.Node for _, m := range p.Modules { var values map[string]any if withValues { if values, err = r.Values(ctx, node, m.Module); err != nil { return compose.Input{}, fmt.Errorf("frombus: %s's config values on %s: %w", m.Module, node, err) } } else if keys := catalogue.ServedSettings(in.Catalogue[m.Module]); len(keys) > 0 { // What a provider elsewhere serves from its settings is told to every consumer: the composing node reads // those keys of it, and no other (issue 1126: mailu's smtp domain, a consumer's on shanks). if values, err = r.Served(ctx, node, m.Module, keys); err != nil { return compose.Input{}, fmt.Errorf("frombus: what %s serves on %s (%s): %w", m.Module, node, strings.Join(keys, ", "), err) } } if layers := m.Layers(node, values); layers != nil { if n.Settings == nil { n.Settings = map[string][]catalogue.Layer{} } n.Settings[m.Module] = layers } } if node == s.Node { for _, module := range s.InOwn { if n.InOwn == nil { n.InOwn = map[string]bool{} } n.InOwn[module] = true } for i := range s.Running { if s.Running[i].Module == "" { own := s.Running[i] n.RunsOwn = &own // Its bus membership as it runs it: the controller composes it from its store, which the node does // not read; until the grants part hands memberships sealed to the node, the node's own composition // carries the one it runs (issue 1126, seed #531 on novox: "bus-membership in what runs, not // composed", which applied would have taken the node's bus credential away). if sealed := membershipOf(own); sealed != "" { if in.BusMemberships == nil { in.BusMemberships = map[string]string{} } in.BusMemberships[s.Node] = sealed } } } } in.Nodes = append(in.Nodes, n) in.Holdings = append(in.Holdings, p.Held()...) // A tunnel found at adoption is the node's private network's when the node's tunnel key is the tunnel's (the // controller's inv.Tunnels); the hub's found tunnel is the mesh's adopted one whether or not the hub took it over // (inv.AdoptedTunnel), and its peers are carried once it did (inv.CarriedPeers). if t := p.Decided.Tunnel; t != nil { if rec.TunnelKey == t.PublicKey { if in.Tunnels == nil { in.Tunnels = map[string]compose.FoundTunnel{} } in.Tunnels[node] = compose.FoundTunnel{Tunnel: tunnelOf(*t), NodeAdopted: p.Decided.Adopted} } if p.Decided.Hub { in.Adopted = &compose.AdoptedTunnel{Tunnel: tunnelOf(*t), Hub: node, Adopted: rec.TunnelKey == t.PublicKey} } } } if in.Adopted != nil && in.Adopted.Adopted { in.CarriedPeers = carriedPeers(in.Adopted.Tunnel, placements[in.Adopted.Hub].Decided.PeerNames, records) } if len(missing) > 0 { return compose.Input{}, fmt.Errorf("frombus: modules are placed on %s, whose agent has written no record yet", strings.Join(missing, ", ")) } if !s.Every { if _, ok := records[s.Node]; !ok { return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node) } if err := fromBuilds(ctx, r, &in, s, placements[s.Node], modules, read, parse, refuse); err != nil { return compose.Input{}, err } // Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5). // Asked only when a module placed on it declares one: a node none of whose modules has a secret composes // whether or not the vault answers. if declaresOwnSecrets(in, s.Node) { if in.OwnSecrets, err = r.OwnSecrets(ctx, s.Node); err != nil { return compose.Input{}, fmt.Errorf("frombus: the own secrets of %s's modules: %w", s.Node, err) } } // The core's credentials travel as the vault's box sealed to the machine (ADR 0349 §16): the same values, which // the composition reads for a core module as its kept boxes. for _, o := range in.OwnSecrets { if json.Valid([]byte(o.Sealed)) { in.KeptCoreBoxes = append(in.KeptCoreBoxes, compose.CoreBox{Node: o.Node, Module: o.Module, Name: o.Name, SealedTo: o.SealedTo, Box: json.RawMessage(o.Sealed)}) } } } return in, nil } // Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.CompositionsApplied at the // fingerprints fps gives each module: composed in the Reading mode as the controller composes them, and refused when it // stands in for anything the input does not carry (compose.ErrStoodIn), since the node applies what it composed. func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (map[string]delivery.Composed, []string, error) { in, err := Input(ctx, r, mesh, s) if err != nil { return nil, nil, err } // The pair credentials its composition reads, asked of the vault sealed to it (ADR 0550 §5). asConsumer, asProvider, err := compose.CredentialsNeeded(in, s.Node) if err != nil { return nil, nil, err } if len(asConsumer)+len(asProvider) > 0 { if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil { return nil, nil, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err) } } return compose.CompositionsApplied(in, s.Node, fps) } // fromBuilds puts in the built manifest of each module placed on the composing node, at the trunk's FP2, and of each // module it keeps at another build (Scope.KeptAt, into its node's Kept), read from the builder's done key and resolved // by the one take-in rule (takein.Manifest), a templates archive read in the artifact store as the node reaches it // (issue 1126). func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module, read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error), refuse func(module string, err error)) error { // stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides // and requires still shapes the rest. stub := func(module string, err error) { refuse(module, err) for i := range in.Nodes { if in.Nodes[i].Name != s.Node { continue } man, ok := in.Nodes[i].Kept[module] if !ok { man, ok = in.Catalogue[module] } if !ok { return } if in.Nodes[i].Kept == nil { in.Nodes[i].Kept = map[string]catalogue.Manifest{} } in.Nodes[i].Kept[module] = compose.Stub(man) } } address, addressErr := "", error(nil) addressed := false archive := func(ctx context.Context, reference, digest string) ([]byte, error) { if !addressed { address, addressErr = compose.ArtifactStoreAddress(*in, s.Node) addressed = true } switch { case addressErr != nil: return nil, addressErr case address == "": return nil, fmt.Errorf("%s reaches no artifact store (%s)", s.Node, storeSought(*in)) } return r.Archive(ctx, catalogue.Routed(reference, address), digest) } built := func(module, fp string, b *delivery.BuilderBuild) (catalogue.Manifest, error) { if b.Manifest == "" { return catalogue.Manifest{}, fmt.Errorf("frombus: the build of %s at %s names no manifest", module, fp) } src, err := read(module, b.Manifest) if err != nil { return catalogue.Manifest{}, err } resolved, err := takein.Manifest(ctx, src, *b, archive) if err != nil { return catalogue.Manifest{}, fmt.Errorf("frombus: %s at %s: %w", module, fp, err) } // Read back as the mesh holds a built manifest (the controller keeps the resolved manifest's bytes and reads them // as any manifest): a template's text is then in the manifest's own words, its ${config:…} in the settings form. raw, err := json.Marshal(resolved) if err != nil { return catalogue.Manifest{}, err } if resolved, err = parse(raw); err != nil { return catalogue.Manifest{}, fmt.Errorf("frombus: the built manifest of %s at %s: %w", module, fp, err) } return resolved, nil } for _, pm := range p.Modules { m, ok := modules[pm.Module] if !ok || m.FP2 == "" || s.KeptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) { // A module it runs inside its own composition is composed there as before: no build of the new form is what // it runs (Scope.InOwn). continue } if man, ok := in.Catalogue[pm.Module]; !ok || (man.Build == nil && !man.Templated()) { continue // nothing is built of it: its manifest is what it composes from } b, err := r.Build(ctx, pm.Module, m.FP2) if err != nil { err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.FP2, err) if refuse == nil { return err } stub(pm.Module, err) continue } if b == nil || b.State != delivery.BuildDone { continue // no done build at the trunk's: composed from the indexer's manifest, as before } resolved, err := built(pm.Module, m.FP2, b) if err != nil { if refuse == nil { return err } stub(pm.Module, err) continue } in.Catalogue[pm.Module] = resolved } for i := range in.Nodes { if in.Nodes[i].Name != s.Node { continue } for _, module := range sortedKeys(s.KeptAt) { fp := s.KeptAt[module] b, err := r.Build(ctx, module, fp) switch { case err != nil: err = fmt.Errorf("frombus: the build of %s at %s: %w", module, fp, err) case b == nil || b.State != delivery.BuildDone: err = fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build", module, fp) } var resolved catalogue.Manifest if err == nil { resolved, err = built(module, fp, b) } if err != nil { if refuse == nil { return err } stub(module, err) continue } if in.Nodes[i].Kept == nil { in.Nodes[i].Kept = map[string]catalogue.Manifest{} } in.Nodes[i].Kept[module] = resolved } } return nil } // declaresOwnSecrets says a module assigned to node declares an own secret, in the manifest the node composes it from. func declaresOwnSecrets(in compose.Input, node string) bool { for _, n := range in.Nodes { if n.Name != node { continue } for _, module := range n.Assigned { m, ok := n.Kept[module] if !ok { m = in.Catalogue[module] } if len(m.OwnSecrets) > 0 { return true } } } return false } // nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person // decided of it, and where its modules run). func nodeOf(rec delivery.NodeRecord, p delivery.Placement) compose.Node { n := compose.Node{ Name: rec.Node, Place: compose.Place{Key: rec.TunnelKey}, SealingKey: rec.SealingKey, OutwardLinks: rec.OutwardLinks, Networks: rec.Networks, NetworksSaid: rec.NetworksSaid, ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: rec.Contract, Stated: rec.Contract > 0, } placeNode(&n, p) if len(rec.Capabilities) > 0 { n.Capabilities = make(map[string]bool, len(rec.Capabilities)) for _, c := range rec.Capabilities { n.Capabilities[c] = true } } return n } // placeNode sets what a person decided of n and where its modules run, from its placement; what the machine observes // (its keys, links, networks, certificate) and its settings layers are left as they are. func placeNode(n *compose.Node, p delivery.Placement) { d := p.Decided n.Place = compose.Place{Key: n.Place.Key, Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address} n.Assigned, n.PublicDomain = p.Assigned(), d.PublicDomain n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentHome n.Adopted, n.Taken = d.Adopted, d.Taken n.Pins, n.Bound, n.Ports = p.ChosenPins(), p.Bound(), p.HostPorts() } // Placed is in with the placements given standing in for those of the nodes they name (issue 1126): each such node's // decided parts, modules, pins, bindings, host ports, settings placement and holdings are the given placement's, its // config values those in already carries for it (the layer of the node's own name), and what the machine observes is // kept. A nil placement leaves the node in the mesh with nothing placed on it. A node in does not hold is refused: it has // no record to compose it from. The found tunnel and the hub's peers are read from in as they are, not from the given // placement. It is how a check composes a change's own placement over an input already filled (composer.Case's // Placements). func Placed(in compose.Input, placements map[string]*delivery.Placement) (compose.Input, error) { if len(placements) == 0 { return in, nil } out := in out.Nodes = append([]compose.Node(nil), in.Nodes...) index := map[string]int{} for i, n := range out.Nodes { index[n.Name] = i } for _, node := range sortedKeys(placements) { p := placements[node] i, ok := index[node] if !ok { return compose.Input{}, fmt.Errorf("frombus: a placement is given for %s, which the input holds no node for", node) } if p == nil { p = &delivery.Placement{Node: node} } if p.Node != node { return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node) } n := out.Nodes[i] placeNode(&n, *p) settings := map[string][]catalogue.Layer{} for _, m := range p.Modules { var values map[string]any for _, l := range n.Settings[m.Module] { if l.From == node { values = l.Values } } if layers := m.Layers(node, values); layers != nil { settings[m.Module] = layers } } n.Settings = nil if len(settings) > 0 { n.Settings = settings } out.Nodes[i] = n } out.Holdings = nil for _, h := range in.Holdings { if _, given := placements[h.Node]; !given { out.Holdings = append(out.Holdings, h) } } for _, node := range sortedKeys(placements) { if p := placements[node]; p != nil { out.Holdings = append(out.Holdings, p.Held()...) } } return out, nil } func tunnelOf(t delivery.RecordTunnel) compose.Tunnel { out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU, Address: t.Address, Range: t.Range, PublicKey: t.PublicKey} for _, p := range t.Peers { out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address}) } return out } // carriedPeers is every peer of the hub's adopted tunnel, by address, each with the node that enrolled under its key // where one has (a node whose tunnel key is the peer's) and the name a person gave it (inv.CarriedPeers). func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]delivery.NodeRecord) []compose.CarriedPeer { byKey := map[string]string{} for node, rec := range records { if rec.TunnelKey != "" { byKey[rec.TunnelKey] = node } } var out []compose.CarriedPeer for _, p := range t.Peers { address, _, _ := strings.Cut(p.Address, "/") out = append(out, compose.CarriedPeer{PublicKey: p.PublicKey, Address: address, EnrolledAs: byKey[p.PublicKey], Named: names[p.PublicKey]}) } sort.SliceStable(out, func(i, j int) bool { a, errA := netip.ParseAddr(out[i].Address) b, errB := netip.ParseAddr(out[j].Address) if errA != nil || errB != nil { return out[i].Address < out[j].Address } return a.Less(b) }) return out } // nodesOf is every node named by a placement or a record, by name: the order the controller reads them in. func nodesOf(placements map[string]delivery.Placement, records map[string]delivery.NodeRecord) []string { seen := map[string]bool{} for n := range placements { seen[n] = true } for n := range records { seen[n] = true } return sortedKeys(seen) } 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 } // storeSought says, for a node that reaches no artifact store, what the composition holds of one: the nodes it holds and // where each module serving the artifact store is placed, so the refusal names why (issue 1126). func storeSought(in compose.Input) string { var nodes, where []string for _, n := range in.Nodes { nodes = append(nodes, n.Name) for _, a := range n.Assigned { if m, ok := in.Catalogue[a]; ok { if _, serves := m.Serves[catalogue.ArtifactStoreProvision]; serves { where = append(where, a+" on "+n.Name) } } } } sort.Strings(nodes) sort.Strings(where) said := "the composition holds " + strings.Join(nodes, ", ") if len(where) == 0 { return said + "; no module placed there serves " + catalogue.ArtifactStoreProvision } return said + "; " + catalogue.ArtifactStoreProvision + " is served by " + strings.Join(where, ", ") + ", none of it on the private network with a port" } // membershipOf is the sealed value of the bus membership resource (catalogue.BusMembershipID) of a node's own // composition, or "". func membershipOf(own delivery.Composed) string { for _, raw := range own.Resources { var r struct { ID string `json:"id"` Sealed string `json:"sealed"` } if json.Unmarshal(raw, &r) == nil && r.ID == catalogue.BusMembershipID() { return r.Sealed } } return "" }