diff --git a/cmd/mesh-control/board.go b/cmd/mesh-control/board.go index 43b7856..a9e25a3 100644 --- a/cmd/mesh-control/board.go +++ b/cmd/mesh-control/board.go @@ -83,13 +83,13 @@ func board() http.Handler { // The same answers for something that is not a person, from the same read. A board and a // script disagreeing about which machine is broken would be worse than either alone. mux.HandleFunc("/mesh.json", func(w http.ResponseWriter, r *http.Request) { - inv, err := openInventory(r.Context()) + open, err := openStores(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } - defer inv.Close() - asked, err := theThreeQuestions(r.Context(), inv) + defer open.Close() + asked, err := theThreeQuestions(r.Context(), open) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return @@ -108,13 +108,13 @@ func board() http.Handler { // ask reads the mesh for one request. func ask(ctx context.Context) (view, error) { - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return view{}, err } - defer inv.Close() + defer open.Close() - asked, err := theThreeQuestions(ctx, inv) + asked, err := theThreeQuestions(ctx, open) if err != nil { return view{}, err } diff --git a/cmd/mesh-control/build.go b/cmd/mesh-control/build.go index 97bf4c0..dfb725d 100644 --- a/cmd/mesh-control/build.go +++ b/cmd/mesh-control/build.go @@ -98,11 +98,12 @@ func buildsCommand(ctx context.Context, args []string) error { return errors.New("builds [] [-n N]") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory builds, err := inv.Builds(ctx, module, *limit) if err != nil { @@ -262,11 +263,12 @@ func builderCommand(ctx context.Context, args []string) error { // reason one broken module no longer blocks a machine's whole declaration: a mesh where one bad // repository holds back nine good ones is a mesh where nobody dares add the tenth. func buildBehind(ctx context.Context, wait time.Duration) error { - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory held, err := inv.Catalogued(ctx) if err != nil { @@ -350,11 +352,12 @@ func buildOne(ctx context.Context, repository, ref string, wait time.Duration) e // Kept before it is judged. A failed build that leaves no trace is indistinguishable from one // nobody asked for, and the difference is the whole of whether somebody should be looking at // something. - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory if err := inv.RecordBuild(ctx, buildFrom(result)); err != nil { return err } diff --git a/cmd/mesh-control/licence.go b/cmd/mesh-control/licence.go index 08f2c00..4ef272d 100644 --- a/cmd/mesh-control/licence.go +++ b/cmd/mesh-control/licence.go @@ -194,11 +194,12 @@ func licenceKey(ctx context.Context, args []string) error { } defer held.Close() - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory sealed, err := held.Accept(ctx, name, value, func(node string) (string, error) { return inv.SealingKeyOf(ctx, node) diff --git a/cmd/mesh-control/modules.go b/cmd/mesh-control/modules.go index 54da37d..53cdf19 100644 --- a/cmd/mesh-control/modules.go +++ b/cmd/mesh-control/modules.go @@ -50,11 +50,12 @@ func moduleCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("module add , module list, or module forget ") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory switch args[0] { case "add": @@ -202,11 +203,12 @@ func assignCommand(ctx context.Context, verb string, args []string) error { if len(args) != 2 { return fmt.Errorf("%s ", verb) } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory if verb == "unassign" { if err := inv.Unassign(ctx, args[0], args[1]); err != nil { @@ -223,7 +225,7 @@ func assignCommand(ctx context.Context, verb string, args []string) error { // Resolved immediately, because an assignment that cannot be applied should be said now // rather than at the next push. The assignment is kept either way: it is what a person meant, // and the refusal is about the set rather than about this one. - if _, _, err := planFor(ctx, inv, args[0]); err != nil { + if _, _, err := planFor(ctx, open, args[0]); err != nil { fmt.Println() return err } @@ -235,11 +237,12 @@ func settingsCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("settings set [--node ], or settings clear [--node ]") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory set := flag.NewFlagSet("settings", flag.ContinueOnError) node := set.String("node", "", "one machine, rather than the whole mesh") @@ -321,11 +324,12 @@ func pinCommand(ctx context.Context, args []string, setting bool) error { if !setting && len(args) != 2 { return errors.New("unpin ") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory if !setting { if err := inv.UnpinProvision(ctx, args[0], args[1]); err != nil { diff --git a/cmd/mesh-control/network.go b/cmd/mesh-control/network.go index 0c59deb..53ce35c 100644 --- a/cmd/mesh-control/network.go +++ b/cmd/mesh-control/network.go @@ -39,17 +39,18 @@ func overlayCommand(ctx context.Context, args []string) error { "what its assignments resolve to — the two are computed from one picture of the " + "mesh, and sending them separately would let them disagree") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory switch args[0] { case "place": return overlayPlace(ctx, inv, args[1:]) case "show": - return overlayShow(ctx, inv) + return overlayShow(ctx, open) default: return fmt.Errorf("overlay has no %q; it has place and show", args[0]) @@ -145,8 +146,9 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, } // graph is the whole mesh's network, for showing it. -func graph(ctx context.Context, inv *inventory.Inventory) ([]overlay.Node, overlay.Graph, error) { - on, refused, err := whoResolves(ctx, inv, overlay.Requirement) +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 } @@ -167,8 +169,9 @@ func graph(ctx context.Context, inv *inventory.Inventory) ([]overlay.Node, overl // 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, inv *inventory.Inventory, requirement string) ( +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 @@ -178,7 +181,7 @@ func whoResolves(ctx context.Context, inv *inventory.Inventory, requirement stri // 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, inv, n.Name) + plan, _, err := planFor(ctx, open, n.Name) if err != nil { refused[n.Name] = err.Error() continue @@ -195,9 +198,10 @@ func whoResolves(ctx context.Context, inv *inventory.Inventory, requirement stri } // rendering is everything a declaration needs, computed over the whole mesh. -func generators(ctx context.Context, inv *inventory.Inventory) ( +func generators(ctx context.Context, open *stores) ( map[string]catalogue.Generator, error) { - on, refused, err := whoResolves(ctx, inv, overlay.Addressing) + inv := open.inventory + on, refused, err := whoResolves(ctx, open, overlay.Addressing) if err != nil { return nil, err } @@ -214,8 +218,8 @@ func generators(ctx context.Context, inv *inventory.Inventory) ( }, nil } -func overlayShow(ctx context.Context, inv *inventory.Inventory) error { - nodes, computed, err := graph(ctx, inv) +func overlayShow(ctx context.Context, open *stores) error { + nodes, computed, err := graph(ctx, open) if err != nil { return err } diff --git a/cmd/mesh-control/nodes.go b/cmd/mesh-control/nodes.go index bbfc053..3907138 100644 --- a/cmd/mesh-control/nodes.go +++ b/cmd/mesh-control/nodes.go @@ -24,11 +24,12 @@ func nodeCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("node add , node list, or node show ") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory switch args[0] { case "show": @@ -89,11 +90,12 @@ func tokenCommand(ctx context.Context, args []string) error { "machine the mesh already has a record for, the second is one it has never seen") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory name := *existing if *fresh != "" { diff --git a/cmd/mesh-control/plan.go b/cmd/mesh-control/plan.go index a206690..bc93c4d 100644 --- a/cmd/mesh-control/plan.go +++ b/cmd/mesh-control/plan.go @@ -21,7 +21,8 @@ import ( // nothing in it was wrong, and no one edit was the one that should have been a new file. // planFor works out everything a node should run, from what was assigned to it. -func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (catalogue.Resolution, catalogue.SettingsBy, error) { +func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Resolution, catalogue.SettingsBy, error) { + inv := open.inventory shelf, err := inv.Catalogue(ctx) if err != nil { return catalogue.Resolution{}, nil, err @@ -63,7 +64,7 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca // What this mesh can answer with a record rather than a machine, and which record each of // this node's modules was put on. Read across a context boundary by name, which is what // crossing one is allowed to carry (novox/hq ADR 0008). - world.Licences, world.Using, err = licencesFor(ctx, nodeName) + world.Licences, world.Using, err = licencesFor(ctx, open, nodeName) if err != nil { return catalogue.Resolution{}, nil, err } @@ -83,7 +84,7 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca // Answered by something the mesh holds, so there is no pair-wise secret between two // machines. Its key was supplied by a person and sealed to this node then; the mesh // discarded the plaintext and cannot make another. - sealed, err := keyFor(ctx, n.From, nodeName, n.For) + sealed, err := keyFor(ctx, open, n.From, nodeName, n.For) if err != nil { return catalogue.Resolution{}, nil, err } @@ -233,21 +234,22 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory, // One place, because there were three and one of them was written before credentials existed and // silently produced a declaration missing them — a difference between what `plan` showed and what // `plan --json` handed to anything reading it. -func declarationFor(ctx context.Context, inv *inventory.Inventory, node string, +func declarationFor(ctx context.Context, open *stores, node string, plan catalogue.Resolution, settings catalogue.SettingsBy) ([]map[string]any, error) { - gens, err := generators(ctx, inv) + gens, err := generators(ctx, open) if err != nil { return nil, err } - return declarationWith(ctx, inv, node, plan, settings, gens) + return declarationWith(ctx, open, node, plan, settings, gens) } // declarationWith is the same, for a caller that has already worked out the generators once and // is about to use them for every node. -func declarationWith(ctx context.Context, inv *inventory.Inventory, node string, +func declarationWith(ctx context.Context, open *stores, node string, plan catalogue.Resolution, settings catalogue.SettingsBy, gens map[string]catalogue.Generator) ([]map[string]any, error) { - grants, err := grantsFor(ctx, inv, node) + inv := open.inventory + grants, err := grantsFor(ctx, open, node) if err != nil { return nil, err } @@ -274,7 +276,7 @@ func declarationWith(ctx context.Context, inv *inventory.Inventory, node string, if m.Certificate == nil { continue } - issued, meshCA, err := certificateFor(ctx, inv, node) + issued, meshCA, err := certificateFor(ctx, open, node) if err != nil { return nil, err } @@ -310,12 +312,12 @@ func declarationWith(ctx context.Context, inv *inventory.Inventory, node string, // the machine and whether it is on the private network, `identity` holds the authority and the // key that machine reported. The process holding both grants asks each for its part // (novox/hq ADR 0008). -func certificateFor(ctx context.Context, inv *inventory.Inventory, node string) (string, string, error) { - ident, err := openIdentity(ctx) +func certificateFor(ctx context.Context, open *stores, node string) (string, string, error) { + inv := open.inventory + ident, err := open.Identity(ctx) if err != nil { return "", "", err } - defer ident.Close() record, err := inv.NodeByName(ctx, node) if err != nil { @@ -371,7 +373,8 @@ func certificateFor(ctx context.Context, inv *inventory.Inventory, node string) // The mirror of what a consumer is given, and the half that makes the credential real: a password // nothing was told to create is a password that authenticates nowhere. Sealed to this node, so // the mesh hands over something it cannot itself use. -func grantsFor(ctx context.Context, inv *inventory.Inventory, node string) ([]catalogue.Grant, error) { +func grantsFor(ctx context.Context, open *stores, node string) ([]catalogue.Grant, error) { + inv := open.inventory issued, err := inv.SecretsFrom(ctx, node) if err != nil { return nil, err @@ -393,7 +396,7 @@ func grantsFor(ctx context.Context, inv *inventory.Inventory, node string) ([]ca // can do nothing with it, and the name a consumer wants is the consumer's to say. out := make([]catalogue.Grant, 0, len(issued)) for _, s := range issued { - plan, settings, err := planFor(ctx, inv, s.Consumer) + plan, settings, err := planFor(ctx, open, s.Consumer) if err != nil { // Their set does not resolve. Skipped rather than fatal: this node is not the place // to report another machine's problem, and a grant for something that is not going to @@ -428,13 +431,13 @@ func planCommand(ctx context.Context, args []string) error { return errors.New("plan [--files] [--json]") } args = positionals - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() - plan, settings, err := planFor(ctx, inv, args[0]) + plan, settings, err := planFor(ctx, open, args[0]) if err != nil { return err } @@ -443,7 +446,7 @@ func planCommand(ctx context.Context, args []string) error { return nil } if *asJSON { - resources, err := declarationFor(ctx, inv, args[0], plan, settings) + resources, err := declarationFor(ctx, open, args[0], plan, settings) if err != nil { return err } @@ -469,7 +472,7 @@ func planCommand(ctx context.Context, args []string) error { for _, n := range plan.Needs { fmt.Printf(" needs %s from %s, for %s\n", n.Name, n.From, n.For) } - resources, err := declarationFor(ctx, inv, args[0], plan, settings) + resources, err := declarationFor(ctx, open, args[0], plan, settings) if err != nil { return err } @@ -497,14 +500,13 @@ func planCommand(ctx context.Context, args []string) error { // A mesh with no licences at all is the ordinary case and must not be an error: every existing // mesh is one, and a control plane that refused to plan because nobody had bought an API key // would be unusable for the thing it already does. -func licencesFor(ctx context.Context, node string) ( +func licencesFor(ctx context.Context, open *stores, node string) ( map[string][]catalogue.Record, map[string]map[string]catalogue.Record, error) { - held, err := openLicences(ctx) + held, err := open.Licences(ctx) if err != nil { return nil, nil, err } - defer held.Close() all, err := held.All(ctx) if err != nil { @@ -546,11 +548,10 @@ func licencesFor(ctx context.Context, node string) ( // **Empty is not an error here.** The mesh discarded the plaintext when it was supplied, so a // holder recorded afterwards genuinely has no key — and the declaration refuses that by name, // where the module and the path are both in view, rather than here. -func keyFor(ctx context.Context, licence, node, module string) (string, error) { - held, err := openLicences(ctx) +func keyFor(ctx context.Context, open *stores, licence, node, module string) (string, error) { + held, err := open.Licences(ctx) if err != nil { return "", err } - defer held.Close() return held.KeyFor(ctx, licence, node, module) } diff --git a/cmd/mesh-control/push.go b/cmd/mesh-control/push.go index e03fe01..cdcb716 100644 --- a/cmd/mesh-control/push.go +++ b/cmd/mesh-control/push.go @@ -25,11 +25,12 @@ import ( // serve is the control plane running: one connection to the broker, one queue, one consumer. func serve(ctx context.Context) error { - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory ident, err := openIdentity(ctx) if err != nil { @@ -100,11 +101,12 @@ func declare(ctx context.Context, args []string) error { // The node has to exist before it can be told anything. Publishing to a queue nobody consumes // would sit there looking like success. - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory if _, err := inv.NodeByName(ctx, node); err != nil { return err } @@ -155,11 +157,12 @@ func pushCommand(ctx context.Context, args []string) error { return errors.New("push or push --behind, not both: one names a machine and the " + "other asks which machines need one") } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory ident, err := openIdentity(ctx) if err != nil { @@ -190,7 +193,7 @@ func pushCommand(ctx context.Context, args []string) error { // only "failed or refused", so a machine that applied cleanly and whose declaration has // since changed was not behind — and novox/hq ADR 0010's question, *did my change go // out?*, was answerable only for the machines that broke. - would, err := wouldSend(ctx, inv, nodes) + would, err := wouldSend(ctx, open, nodes) if err != nil { return err } @@ -211,7 +214,7 @@ func pushCommand(ctx context.Context, args []string) error { return nil } } - gens, err := generators(ctx, inv) + gens, err := generators(ctx, open) if err != nil { return err } @@ -254,7 +257,7 @@ func pushCommand(ctx context.Context, args []string) error { n.Name, doing.Outcome, doing.At.Local().Format("2006-01-02 15:04")) } } - plan, settings, err := planFor(ctx, inv, n.Name) + plan, settings, err := planFor(ctx, open, n.Name) if err != nil { refusals = append(refusals, fmt.Sprintf("%s:\n%v", n.Name, err)) continue @@ -262,7 +265,7 @@ func pushCommand(ctx context.Context, args []string) error { // The private network is in here with everything else. It used to be composed separately // and prepended, which meant every machine with an address was on it and no machine could // be kept off. It is a module now, so it arrives the way a module does. - resources, err := declarationWith(ctx, inv, n.Name, plan, settings, gens) + resources, err := declarationWith(ctx, open, n.Name, plan, settings, gens) if err != nil { refusals = append(refusals, fmt.Sprintf("%s:\n%v", n.Name, err)) continue @@ -307,14 +310,15 @@ func pushCommand(ctx context.Context, args []string) error { // The same all-or-nothing rule push follows, and for the same reason: a rotation that reached the // consumer and refused on the provider would leave one end holding a credential the other has // never heard of — which is the state this whole mechanism exists to make impossible. -func sendTo(ctx context.Context, inv *inventory.Inventory, names []string) error { +func sendTo(ctx context.Context, open *stores, names []string) error { + inv := open.inventory ident, err := openIdentity(ctx) if err != nil { return err } defer ident.Close() - gens, err := generators(ctx, inv) + gens, err := generators(ctx, open) if err != nil { return err } @@ -326,12 +330,12 @@ func sendTo(ctx context.Context, inv *inventory.Inventory, names []string) error var sending []ready var refusals []string for _, name := range names { - plan, settings, err := planFor(ctx, inv, name) + plan, settings, err := planFor(ctx, open, name) if err != nil { refusals = append(refusals, fmt.Sprintf("%s:\n%v", name, err)) continue } - resources, err := declarationWith(ctx, inv, name, plan, settings, gens) + resources, err := declarationWith(ctx, open, name, plan, settings, gens) if err != nil { refusals = append(refusals, fmt.Sprintf("%s:\n%v", name, err)) continue @@ -382,20 +386,20 @@ func digestOf(body []byte) string { // // Machines that do not resolve are left out rather than reported as waiting: "this machine cannot // be worked out" is a different problem with a different remedy, and `plan` is where it is said. -func wouldSend(ctx context.Context, inv *inventory.Inventory, +func wouldSend(ctx context.Context, open *stores, nodes []inventory.Node) (map[string]string, error) { - gens, err := generators(ctx, inv) + gens, err := generators(ctx, open) if err != nil { return nil, err } out := map[string]string{} for _, n := range nodes { - plan, settings, err := planFor(ctx, inv, n.Name) + plan, settings, err := planFor(ctx, open, n.Name) if err != nil { continue } - resources, err := declarationWith(ctx, inv, n.Name, plan, settings, gens) + resources, err := declarationWith(ctx, open, n.Name, plan, settings, gens) if err != nil { continue } diff --git a/cmd/mesh-control/rotate.go b/cmd/mesh-control/rotate.go index db303d5..a69aab3 100644 --- a/cmd/mesh-control/rotate.go +++ b/cmd/mesh-control/rotate.go @@ -42,11 +42,12 @@ func rotateCommand(ctx context.Context, args []string) error { } provision := positionals[0] - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory holders, err := inv.HoldersOf(ctx, provision, *only) if err != nil { @@ -101,7 +102,7 @@ func rotateCommand(ctx context.Context, args []string) error { // honest thing is to make it as short as the broker allows and to never leave it open across // a command boundary, where it depends on somebody's memory. fmt.Printf("\nsending to both ends:\n") - if err := sendTo(ctx, inv, machines); err != nil { + if err := sendTo(ctx, open, machines); err != nil { return fmt.Errorf( "%w\n\nThe old credentials are gone from the mesh and the new ones have not arrived. "+ "Nothing on those machines has changed yet, so what is running keeps working "+ diff --git a/cmd/mesh-control/status.go b/cmd/mesh-control/status.go index 69978b3..5cd20ea 100644 --- a/cmd/mesh-control/status.go +++ b/cmd/mesh-control/status.go @@ -40,13 +40,13 @@ func statusCommand(ctx context.Context, args []string) error { return err } - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() - asked, err := theThreeQuestions(ctx, inv) + asked, err := theThreeQuestions(ctx, open) if err != nil { return err } @@ -171,7 +171,8 @@ type builds struct{ inv *inventory.Inventory } // The order is the design and not a convenience: is anything broken, is anything not answering, is // anything out of date. The first has consequences now, the second may, the third is a plan for // later — and anything that led with the third would bury the first. -func theThreeQuestions(ctx context.Context, inv *inventory.Inventory) (answers, error) { +func theThreeQuestions(ctx context.Context, open *stores) (answers, error) { + inv := open.inventory var out answers var err error @@ -198,7 +199,7 @@ func theThreeQuestions(ctx context.Context, inv *inventory.Inventory) (answers, // And which machines are not running what the mesh would send them. The same question as a // module being behind its source, one level down: that one says the catalogue is out of date, // this one says a machine is — and only the second has anybody's change waiting in it. - would, err := wouldSend(ctx, inv, out.nodes) + would, err := wouldSend(ctx, open, out.nodes) if err != nil { return answers{}, err } diff --git a/cmd/mesh-control/stores.go b/cmd/mesh-control/stores.go index 69263f6..5c467b2 100644 --- a/cmd/mesh-control/stores.go +++ b/cmd/mesh-control/stores.go @@ -60,11 +60,12 @@ func migrate(ctx context.Context) error { // The modules the control plane ships with itself. Recorded here rather than by hand, because // a mesh whose own private network is missing from the catalogue would have nothing to assign // and no way to say why. - inv, err := openInventory(ctx) + open, err := openStores(ctx) if err != nil { return err } - defer inv.Close() + defer open.Close() + inv := open.inventory for _, m := range provided { if err := inv.Provide(ctx, m); err != nil { return err @@ -111,3 +112,67 @@ func openLicences(ctx context.Context) (*licences.Licences, error) { } return held, nil } + +// stores is what one command has open. +// +// **Opened once, not once per machine.** Working out what a machine should be reaches the identity +// context for its certificate and the licence context for its model access, and both were opened — +// and waited on — inside functions called for every node in a push. Two machines hid it; fifty +// would be fifty connect-and-wait cycles for data that does not change while the push runs. +// +// Each is opened on first use rather than up front, because most commands need one context and +// paying to reach three would be the same waste from the other side. +type stores struct { + inventory *inventory.Inventory + identity *identity.Identity + licences *licences.Licences +} + +// open connects to the inventory, which every command that touches the mesh needs. +func openStores(ctx context.Context) (*stores, error) { + inv, err := openInventory(ctx) + if err != nil { + return nil, err + } + return &stores{inventory: inv}, nil +} + +// Identity is this control plane's own identity context, opened if it has not been. +func (h *stores) Identity(ctx context.Context) (*identity.Identity, error) { + if h.identity != nil { + return h.identity, nil + } + opened, err := openIdentity(ctx) + if err != nil { + return nil, err + } + h.identity = opened + return opened, nil +} + +// Licences is the context holding model access, opened if it has not been. +func (h *stores) Licences(ctx context.Context) (*licences.Licences, error) { + if h.licences != nil { + return h.licences, nil + } + opened, err := openLicences(ctx) + if err != nil { + return nil, err + } + h.licences = opened + return opened, nil +} + +// Close lets go of everything that was opened, in any order: they are separate connections to +// separate databases and none of them knows about the others. +func (h *stores) Close() { + if h.licences != nil { + h.licences.Close() + } + if h.identity != nil { + h.identity.Close() + } + if h.inventory != nil { + h.inventory.Close() + } +}