package main import ( "context" "errors" "flag" "fmt" "os" "sort" "strconv" "strings" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/overlay" ) // the private network: who is on it, where, and what they are called. // // Split out of main.go, which had reached 2,769 lines because appending was always the // cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636: // nothing in it was wrong, and no one edit was the one that should have been a new file. // DefaultOverlayCIDR is the range the mesh allocates from when nothing says another. const DefaultOverlayCIDR = "10.42.0.0/16" // overlayRange is the range the mesh allocates node addresses from. // // **The adopted tunnel's range first** (novox/hq ADR 0105): a hub that took over the tunnel it // found is at that tunnel's address, its peers are at theirs, and every node's address is // composed from the same range — the hub's, and every binding, hosts entry and endpoint derived // from it. Those are readers of this; none of them stores the range. Without an adopted tunnel, // the range genesis was told, or the default. func overlayRange(ctx context.Context, inv *inventory.Inventory) (string, error) { tunnel, _, adopted, err := inv.AdoptedTunnel(ctx) if err != nil { return "", err } if adopted { return tunnel.Range, nil } if v := strings.TrimSpace(os.Getenv(OverlayCIDRVar)); v != "" { return v, nil } return DefaultOverlayCIDR, nil } func overlayCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("overlay place [flags], overlay name
, or overlay show") } // Answered before anything is opened. A message about which command to use should not need a // database to say so, and needing one turns a redirect into a connection error. if args[0] == "push" { return errors.New("`overlay push` is now `push`, which sends a node its network AND " + "what its assignments resolve to — the two are computed from one picture of the " + "mesh, and sending them separately would let them disagree") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory switch args[0] { case "place": return overlayPlace(ctx, inv, args[1:]) case "show": return overlayShow(ctx, open) case "name": return overlayName(ctx, inv, args[1:]) default: return fmt.Errorf("overlay has no %q; it has place, name and show", args[0]) } } // overlayName is the operator saying which machine a carried address is (novox/hq issue 112), // so the mesh answers for its name until the machine enrols and verifies it. func overlayName(ctx context.Context, inv *inventory.Inventory, args []string) error { if len(args) != 2 { return errors.New("overlay name ") } address, name := args[0], args[1] if err := inv.NamePeer(ctx, address, name); err != nil { return err } fmt.Printf("the peer at %s is %s until it enrols — the mesh answers for %s. from the "+ "operator's word, and enrolment under this key must use this name\n", address, name, name) return nil } func overlayPlace(ctx context.Context, inv *inventory.Inventory, args []string) error { if len(args) == 0 { return errors.New( "overlay place [--endpoint host:port] [--site name] [--hub], or --nothing") } node := args[0] set := flag.NewFlagSet("overlay place", flag.ContinueOnError) endpoint := set.String("endpoint", "", "where this node can be dialled, or empty for nowhere") site := set.String("site", "", "where this machine physically is, or empty if it roams") hub := set.Bool("hub", false, "this node is the hub every other routes through") nothing := set.Bool("nothing", false, "place it with nothing set: not dialable, no site, not the hub") if err := set.Parse(args[1:]); err != nil { return err } // **All three are declared together, so saying nothing took all three away.** The sibling of // `node public-domain`: `overlay place anchor` reads like it places the node it names, and it // silently unset the endpoint every other machine dials, the site it is in, and the hub if it // was the hub — every path through it going with them, at the moment somebody was trying to // look at it. // // A placement with nothing set is a real thing to want — a machine that roams and opens every // path itself is exactly that — so it keeps a way to say so, by name. if set.NFlag() == 0 { return fmt.Errorf("overlay place %s was given nothing to place it with, and all three are "+ "declared together — it would take away the endpoint other machines dial %s at, its "+ "site, and the hub if it is the hub. Say --endpoint/--site/--hub, or --nothing if that "+ "is what you meant", node, node) } if *nothing && (*endpoint != "" || *site != "" || *hub) { return fmt.Errorf("give %s a placement or --nothing, not both: they say opposite things "+ "and the mesh will not choose between them", node) } // One hub per mesh, refused rather than last-write-wins: with two flagged, which one the // graph and the broker address pick is order-dependent — the silent-election fault ADR 0007 // exists to avoid, one flag over (novox/hq issue 059). Moving the hub is explicit: re-place // the old one without --hub first. if *hub { placed, err := inv.Overlays(ctx) if err != nil { return err } for _, p := range placed { if p.Hub && p.Name != node { return fmt.Errorf("%s is already the hub; a mesh has one. Re-place %s without "+ "--hub first if the hub is moving", p.Name, p.Name) } } } // A hub that took over a tunnel listens on that tunnel's port — it is what the peers dial, and // the reason the port is worth having (novox/hq ADR 0105). An endpoint on another port would // have the mesh's interface up where no peer is listening for it. found, err := inv.NodeByName(ctx, node) if err != nil { return err } var tunnel inventory.Tunnel adoptsTunnel := false if *hub && found.Adopted { if t, err := inv.TunnelOf(ctx, node); err == nil { tunnel = t placed, _ := inv.Overlays(ctx) for _, o := range placed { if o.Name == node && o.Key == t.PublicKey { adoptsTunnel = true } } } else if !errors.Is(err, inventory.ErrNoTunnel) { return err } } if adoptsTunnel { if port := portOfEndpoint(*endpoint); port != strconv.Itoa(tunnel.Port) { return fmt.Errorf("%s takes over the tunnel it found on %s, which listens on port %d, and "+ "its endpoint %q names another port: the peers dial the tunnel's port, so the hub's "+ "endpoint must be on it", node, tunnel.Interface, tunnel.Port, *endpoint) } } // Declared, all three. The address is evidence of reachability and is not the fact, and hub // election by address prefix fails silently (novox/hq ADR 0007). if err := inv.SetPlace(ctx, node, *endpoint, *site, *hub, ""); err != nil { return err } cidr, err := overlayRange(ctx, inv) if err != nil { return err } address, err := inv.AssignAddress(ctx, found.ID, cidr) if err != nil { return err } fmt.Printf("%s is at %s on the overlay\n", node, address) fmt.Println(" credentials issued for it before this placement keep their old broker address —" + " `module issue` them again and push (novox/hq issue 059)") switch { case adoptsTunnel: fmt.Printf(" the hub — it takes over the tunnel it found on %s: range %s, port %d, "+ "%d peer(s) carried until they enrol\n", tunnel.Interface, tunnel.Range, tunnel.Port, len(tunnel.Peers)) case *hub: fmt.Println(" the hub — every node not sharing a site routes through it") case *endpoint == "": fmt.Println(" not dialable — it opens every path itself") } if *site != "" { fmt.Printf(" at %s, so it peers directly with anything else there\n", *site) } return nil } // network builds the private network over the machines that resolved the module for it. // // Not over every node the mesh knows. **A machine is on the private network because it was given // the module**, and one that was not is absent from every peer list and from the names — which is // the only thing "not on the network" can mean. Until this, having an address was enough, and // there was no way to keep a machine off. // // Every node at once, which is the whole reason this is the control plane's work: a peer list is // derived from all the others, so no node could compute its own. func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, refused map[string]string) (*overlay.Generator, error) { places, err := inv.Overlays(ctx) if err != nil { return nil, err } // The tunnels adopted nodes take over, and the peers the hub's carries (novox/hq ADR 0105). tunnels, err := inv.Tunnels(ctx) if err != nil { return nil, err } carried, err := inv.CarriedPeers(ctx) if err != nil { return nil, err } nodes := make([]overlay.Node, 0, len(places)) for _, p := range places { if !on[p.Name] { continue } n := overlay.Node{ Name: p.Name, Key: p.Key, Endpoint: p.Endpoint, Site: p.Site, Hub: p.Hub, Address: p.Address, } if t, takes := tunnels[p.Name]; takes && t.NodeAdopted { // Only an adopted node is told to take the found unit over: on a converged one there // is nothing found to keep, and the host refuses the field. The range and the carried // peers do not depend on the mode; the takeover does. // // **Refused, not composed, when the hub's record disagrees with the tunnel.** A // declaration that stopped the found unit and raised the mesh's interface on another // port or address would leave every peer dark while reporting the tunnel taken — so a // hub placed before it took the tunnel over (or at the wrong port) is named here, and // nothing is sent until it is re-placed. if wrong := disagrees(p, t.Tunnel); wrong != "" { return nil, fmt.Errorf("%s takes over the tunnel on %s and its placement disagrees with it: %s. "+ "Re-place it — `overlay place %s --hub --endpoint :%d …` — and push again; "+ "nothing was composed", p.Name, t.Interface, wrong, p.Name, t.Port) } n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU} } if p.Hub { for _, c := range carried { n.Carried = append(n.Carried, overlay.Carried{Key: c.PublicKey, Address: c.Address}) } } nodes = append(nodes, n) } if len(nodes) == 0 { // Nobody was given it. An empty network is a legitimate mesh, not a broken one, so this // answers rather than refusing -- Compute would refuse for want of a hub, and reporting // "no hub" to somebody who never asked for a network would be a lie about the cause. return overlay.Empty(), nil } cidr, err := overlayRange(ctx, inv) if err != nil { return nil, err } g, err := overlay.From(nodes, cidr, "") if g != nil { // The artifact store, as this network reaches it. Found rather than configured: the // provider is whichever module offers it, on whichever machine holds that module — and if // nothing does yet (genesis raises the registry before the catalogue knows it), there is // no trust to write and nothing is written (novox/hq ADR 0082). // // Refused rather than composed without it when the question could not be answered: a // declaration missing the trust because a lookup failed is a machine that cannot pull, // delivered by a push that reported success — and nothing recomposes it until the next // push (the shape of novox/hq issues 042/048, reappearing as a race). at, port, found, storeErr := artifactStoreOnNetwork(ctx, inv, on) if storeErr != nil { return nil, fmt.Errorf("finding the artifact store this network reaches: %w", storeErr) } if found { g.TrustRegistry(overlay.InternalName(at) + ":" + port) } } if err != nil && len(refused) > 0 { // The network is missing something, and some machines could not be resolved at all. Those // are almost always the same fact: a node that does not resolve contributes nothing, so // reporting "no hub" would name a consequence and hide the cause. var who []string for name, why := range refused { who = append(who, fmt.Sprintf(" %s: %s", name, why)) } sort.Strings(who) return nil, fmt.Errorf("%w\n\nand %d node(s) could not be resolved at all, which is "+ "probably why:\n%s", err, len(refused), strings.Join(who, "\n")) } return g, err } // graph is the whole mesh's network, for showing it. func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, error) { inv := open.inventory on, refused, err := whoResolves(ctx, open, overlay.Requirement) if err != nil { return nil, nil, err } g, err := network(ctx, inv, on, refused) if err != nil { return nil, nil, err } return g.Nodes(), g.Graph(), nil } // whoResolves is the machines whose resolution answers a requirement, and why the others did not. // // By what a module **provides**, not by its name. WireGuard is one way to have a private network // and there could be others, so a machine is on the network because something it runs provides // one — asking for a particular module by name would be the mistake this whole mechanism exists // to avoid. // // Resolved rather than read from the assignment table, because a module can arrive by being // required by something else, and a machine that needs the private network to do its job is on it // for the same reason as one that was handed it directly. func whoResolves(ctx context.Context, open *stores, requirement string) ( map[string]bool, map[string]string, error) { inv := open.inventory nodes, err := inv.Nodes(ctx) if err != nil { return nil, nil, err } on := map[string]bool{} // Why a node could not be resolved, kept rather than raised: one broken node must not stop // the rest being described, and whoever is rendering that node will raise it themselves. refused := map[string]string{} for _, n := range nodes { plan, _, err := planFor(ctx, open, n.Name) if err != nil { refused[n.Name] = err.Error() continue } for _, m := range plan.Modules { for _, offered := range m.Offers() { if offered == requirement { on[n.Name] = true } } } } return on, refused, nil } // rendering is everything a declaration needs, computed over the whole mesh. func generators(ctx context.Context, open *stores) ( map[string]catalogue.Generator, error) { inv := open.inventory on, refused, err := whoResolves(ctx, open, overlay.Addressing) if err != nil { return nil, err } net, err := network(ctx, inv, on, refused) if err != nil { return nil, err } // **One generator now.** Two more used to sit beside it — the names and a resolver's zone // file — as modules that ran nothing. Both are facts a module asks for in its manifest // (`facts:` — catalogue.FactsInto), computed from the same machines this sees: the ones on the // private network, because a name for a machine not on it would resolve to an address nothing // can reach. return map[string]catalogue.Generator{ overlay.Name: net, }, nil } func overlayShow(ctx context.Context, open *stores) error { nodes, computed, err := graph(ctx, open) if err != nil { return err } if len(nodes) == 0 { // Not "this mesh has no nodes", which it said until the network became a module and was // then a lie about the cause: a mesh can have every node it will ever have and nobody on // the private network, because nobody asked for one. fmt.Printf("nobody is on the private network — assign %s to put a machine on it\n", overlay.Name) return nil } // The tunnel the hub took over, if any, and the peers carried from it (novox/hq ADR 0105): // listed apart from the nodes, because they are peers of the tunnel and not nodes of the // mesh until they enrol — and once one has, it is listed as the node it became. tunnel, hubName, adopted, err := open.inventory.AdoptedTunnel(ctx) if err != nil { return err } carried, err := open.inventory.CarriedPeers(ctx) if err != nil { return err } for _, n := range nodes { place := n.Address if place == "" { // Said, not skipped. A node with no place is a node with no network, and it should // be visible here rather than quietly absent from a list of who is on it. place = "no address — run `overlay place`" } fmt.Printf("%-16s %-14s", n.Name, place) switch { case n.Hub && adopted: fmt.Printf(" hub — over the tunnel it took over on %s (range %s, port %d)", tunnel.Interface, tunnel.Range, tunnel.Port) case n.Hub && hubName == n.Name && tunnel.Interface != "": fmt.Printf(" hub — found a tunnel on %s and did NOT take it over: its key is not the tunnel's; "+ "`mesh-host overlay take --tunnel %s` on the machine takes it", tunnel.Interface, tunnel.Interface) case n.Hub: fmt.Print(" hub — found no tunnel; if the machine runs the predecessor's, " + "`mesh-host overlay take --tunnel ` there adopts it (novox/hq ADR 0105)") case !n.Reachable(): fmt.Print(" not dialable") } if n.Site != "" { fmt.Printf(" at %s", n.Site) } if n.TakesOver != nil && !n.Hub { fmt.Printf(" takes over %s", n.TakesOver.Interface) } fmt.Println() for _, p := range computed[n.Name] { fmt.Printf(" → %-14s %-18s %s\n", p.Name, p.Allowed, p.Why) } } if len(carried) > 0 { fmt.Printf("\npeers of the tunnel %s took over — not nodes of the mesh until they enrol:\n", hubName) for _, c := range carried { state := "not yet enrolled" if c.EnrolledAs != "" { state = "enrolled as " + c.EnrolledAs + ", which keeps this address" } fmt.Printf(" %-16s %-14s %s\n", overlay.CarriedName(c.PublicKey), c.Address, state) } } return nil } // disagrees says how a node's placement differs from the tunnel it takes over — its address not // the tunnel's, its endpoint not on the tunnel's port — or nothing when both agree. func disagrees(p inventory.Overlay, t inventory.Tunnel) string { var wrong []string want := t.Address if i := strings.Index(want, "/"); i >= 0 { want = want[:i] } if p.Address != want { wrong = append(wrong, fmt.Sprintf("its address is %s and the tunnel's is %s", orNothing(p.Address), want)) } if p.Reachable() && portOfEndpoint(p.Endpoint) != strconv.Itoa(t.Port) { wrong = append(wrong, fmt.Sprintf("its endpoint %s is not on the tunnel's port %d", p.Endpoint, t.Port)) } return strings.Join(wrong, "; ") } func orNothing(s string) string { if s == "" { return "unset" } return s } // portOfEndpoint is the port in host:port, or empty. func portOfEndpoint(endpoint string) string { if i := strings.LastIndex(endpoint, ":"); i >= 0 { return endpoint[i+1:] } return "" } // SilentFor is how long a node may be quiet before the mesh says so. // // A node speaks every minute, so three of them missed is a gap rather than a slow one. The number // is not the point — being able to say "out of touch" at all is, and nothing could before. const SilentFor = 3 * time.Minute // whereEveryoneIs is each machine's name on the private network, for the ones on it. // // **Resolved without consulting the rest of the mesh**, and that is not an optimisation. Every // other path here answers a question about one node by resolving the others; this one is called // *from* that path, so doing the same would not terminate — which it did not, for two minutes, // until it was run. // // An unchecked resolution is exactly right for the question anyway. Whether a machine is on the // private network depends on what it was assigned and what that requires, both of which are local // facts. What it takes *from* other machines does not change the answer. // // The distinction that matters is kept: a machine absent from the network module's own view is // absent here, so "has an address" is not mistaken for "is reachable" — which it was, before the // network became something a machine is given. func whereEveryoneIs(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest) (map[string]string, error) { if shelf == nil { // Refused rather than answered. Being on the private network is a conclusion about what a // node resolves to, so with no catalogue nothing resolves and the honest answer is // "nobody" — which is wrong, indistinguishable from a mesh with no overlay, and refused // every certificate the mesh was asked for while saying the machine was on no network. return nil, errors.New( "asked where everyone is without the catalogue, which cannot be answered") } places, err := onTheNetwork(ctx, inv, shelf) if err != nil { return nil, err } out := map[string]string{} for _, p := range places { out[p.Name] = overlay.InternalName(p.Name) } return out, nil } // onTheNetwork is every placed machine that resolves the private network — has an address AND // runs what puts it there. "Has an address" alone was true of every placed machine and told you // nothing about whether anything could reach it; a name written for such a machine resolves to // an address that does not answer, and a connection to it hangs (novox/hq issue 079). func onTheNetwork(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest) ([]inventory.Overlay, error) { places, err := inv.Overlays(ctx) if err != nil { return nil, err } var out []inventory.Overlay for _, p := range places { if p.Address == "" { continue } assigned, err := inv.Assigned(ctx, p.Name) if err != nil || len(assigned) == 0 { continue } caps, _ := inv.ProfileOf(ctx, p.Name) got, err := catalogue.Resolve(shelf, assigned, catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: caps}, catalogue.World{Unchecked: true}) if err != nil { continue } for _, m := range got.Modules { for _, offered := range m.Offers() { if offered == overlay.Requirement { out = append(out, p) } } } } return out, nil } // onThePrivateNetwork is every node's address on the private network, sorted — the same set // the names and the resolver mean by it (onTheNetwork), so a rule saying "from the mesh" admits // exactly the machines the mesh names. // // A node with no address is left out rather than rendered as an empty source: an empty entry in a // source set is a syntax error in the rule file, and a rule file that does not load leaves the // node filtering whatever it was filtering before -- the one outcome worse than a wrong rule, // because nothing reports it. func onThePrivateNetwork(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest) ([]string, error) { places, err := onTheNetwork(ctx, inv, shelf) if err != nil { return nil, err } var out []string for _, p := range places { if strings.TrimSpace(p.Address) != "" { out = append(out, p.Address) } } sort.Strings(out) return out, nil } // namesInTheMesh is every machine's internal name and the address behind it — every machine // that is on the private network, the same set the resolver means by that. // // A machine that is not has no name: writing one that resolves to nothing is worse than not // writing it, because a connection to an address that does not answer hangs where a name that // does not resolve fails at once and says so. A machine placed on the overlay but not running // the module that puts it there is exactly that (novox/hq issue 079). func namesInTheMesh(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest) (map[string]string, error) { places, err := onTheNetwork(ctx, inv, shelf) if err != nil { return nil, err } out := map[string]string{} for _, p := range places { out[overlay.InternalName(p.Name)] = p.Address } // And the carried peers the operator has named (novox/hq issue 112): machines the // predecessor's resolver answers for and the mesh routes to, known by name on the operator's // word until they enrol — at which point enrolment verifies the name and the node's own // entry takes over above. A name the predecessor answers for must keep resolving until the // machine behind it is a node; without these, taking the resolver silences three machines. carried, err := inv.CarriedPeers(ctx) if err != nil { return nil, err } for _, p := range carried { if p.Named == "" || p.EnrolledAs != "" { continue } if _, taken := out[overlay.InternalName(p.Named)]; taken { continue // a node of the mesh owns the name; the stale statement loses } out[overlay.InternalName(p.Named)] = p.Address } return out, nil } // artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the // port THAT MACHINE put it on — the node's setting when it was given one (novox/hq ADR 0100, // 04-ISSUES/102), the mesh's assignment when it made one, and the manifest's own number only when // neither says anything. Read exactly as a consumer's binding is, because the trust a machine // writes for the store and the address it pulls from are the same fact as what a consumer is told. // // A lookup failure is an error, never "not found": collapsing the two composed a declaration // without the trust whenever the inventory hiccuped, delivered by a push that reported success — // and nothing recomposed the machine until the next push. "No store" must mean the mesh has none, // not that the question went unanswered. func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory, on map[string]bool) (node, port string, found bool, err error) { shelf, err := inv.Catalogue(ctx) if err != nil { return "", "", false, fmt.Errorf("reading the catalogue: %w", err) } providers := map[string]catalogue.Manifest{} for name, m := range shelf { if _, offers := m.Serves[catalogue.ArtifactStoreProvision]; offers { providers[name] = m } } if len(providers) == 0 { return "", "", false, nil } // In a stated order, so two machines offering it would always answer the same one. machines := make([]string, 0, len(on)) for machine := range on { machines = append(machines, machine) } sort.Strings(machines) for _, machine := range machines { assigned, err := inv.Assigned(ctx, machine) if err != nil { return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err) } for _, a := range assigned { m, offers := providers[a] if !offers { continue } serves, err := servedOnNode(ctx, inv, machine, m, catalogue.ArtifactStoreProvision) if err != nil { return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err) } if p, ok := serves["port"]; ok { return machine, fmt.Sprintf("%v", p), true, nil } } } return "", "", false, nil } // artifactStoreAddress is the artifact store as `forNode` reaches it: `.internal:` // over the private network, or — when nothing is on the network yet — `127.0.0.1:` for the // node that holds the store itself, and "" for any other. The address composed into every // reference the mesh built, at the moment it is used and never before (novox/hq 04-ISSUES/102). // // **Genesis places the network after the store, the broker, the vault and the catalogue.** Each // of those is built and pushed to a node that is on no network, and the store is on that same // node; an answer of "no store" there would refuse every one of those pushes and hand every one // of those builds a base nothing can pull. Loopback is the truth on that machine, and it is the // address genesis itself reaches the store by. func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest, forNode string) (string, error) { onNetwork, err := whereEveryoneIs(ctx, inv, shelf) if err != nil { return "", err } on := map[string]bool{} for name := range onNetwork { on[name] = true } node, port, found, err := artifactStoreOnNetwork(ctx, inv, on) if err != nil { return "", err } if found { return overlay.InternalName(node) + ":" + port, nil } holder, port, found, err := artifactStoreHolder(ctx, inv) if err != nil || !found || holder != forNode { return "", err } return "127.0.0.1:" + port, nil } // artifactStoreHolder is whichever node is assigned a module offering the artifact store, on or // off the network, and the port that node put it on. func artifactStoreHolder(ctx context.Context, inv *inventory.Inventory) (node, port string, found bool, err error) { nodes, err := inv.Nodes(ctx) if err != nil { return "", "", false, err } all := map[string]bool{} for _, n := range nodes { all[n.Name] = true } return artifactStoreOnNetwork(ctx, inv, all) }