package main import ( "context" "fmt" "os" "sync" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/identity" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/licences" "github.com/novox/mesh-controller/internal/store" ) // reaching each context's store, which no other context may touch. // // 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. // migrate brings every held context's schema up to date. // // Reported per context and per migration, because this runs during a bootstrap on a machine with // nothing else on it — the output is the only account of what happened, and "migrated" is not one. func migrate(ctx context.Context) error { for _, c := range held { migrations, err := c.migrations() if err != nil { return err } s, err := store.Open(ctx, c.name) if err != nil { return err } defer s.Close() // The bootstrap raises PostgreSQL moments before this runs, and a container that is // running is not a database that will answer — a distinction this project has already // paid for once, when a crash-looping database reported itself as up between restarts. if err := s.Ready(ctx, 60*time.Second); err != nil { return err } done, err := s.Migrate(ctx, migrations) for _, m := range done { fmt.Printf("%s: applied %04d-%s\n", c.name, m.Number, m.Name) } if err != nil { return err } if len(done) == 0 { applied, err := s.AppliedMigrations(ctx) if err != nil { return err } fmt.Printf("%s: already up to date — %d migration(s)\n", c.name, len(applied)) } } // 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. open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory for _, m := range provided { if err := inv.Provide(ctx, m); err != nil { return err } fmt.Printf("provided %s\n", m.Module) } // And what an earlier release shipped and this one does not goes (novox/hq ADR 0226) — unless a // machine still has it, which is said rather than overridden. var shipped []string for _, m := range provided { shipped = append(shipped, m.Module) } retired, kept, err := inv.RetireUnshipped(ctx, shipped) if err != nil { return err } for _, name := range retired { fmt.Printf("retired %s: the control plane no longer ships it\n", name) } for name, why := range kept { fmt.Printf("kept %s, which the control plane no longer ships: %s\n", name, why) } // The seats the mesh ships with, into the table that now holds the set (novox/hq ADR 0122). // Idempotent: fills an empty table on first boot, adds a seat a release ships, and leaves an // operator's changes in the table as they are. added, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()) if err != nil { return err } fmt.Printf("seeded %d seat(s)\n", added) return nil } // openInventory connects and waits, the way every command that touches it needs to. func openInventory(ctx context.Context) (*inventory.Inventory, error) { inv, err := inventory.Open(ctx) if err != nil { return nil, err } if err := inv.Ready(ctx, 30*time.Second); err != nil { inv.Close() return nil, err } // A plan is written only under the lease, carrying its epoch (novox/hq to-be 45 §6). inv.ActsUnder(theLease.epoch) // Load the seat set from the store, so the control plane reads the set as data rather than as // the slice it was compiled with (novox/hq ADR 0122). A store not yet seeded — or one whose // seat table a migration has not reached — returns nothing, and UseSeats leaves the compiled // defaults in force: the set is never emptied by a read that found nothing, which would refuse // every claim. So this can only ever replace the defaults with what the mesh actually holds. if seats, err := inv.Seats(ctx); err == nil { catalogue.UseSeats(seats) } // And the former names, so a reference to a seat's old name resolves after a rename (ADR 0122). if aliases, err := inv.Aliases(ctx); err == nil { catalogue.UseAliases(aliases) } // And the list of predefined keys the mesh publishes (novox/hq ADR 0348 §7), which every manifest read again // is judged against. A store a migration has not reached yet keeps none, and nothing names a key then. if err := loadPredefined(ctx, inv); err != nil { fmt.Fprintf(os.Stderr, "the list of predefined keys kept cannot be read: %v\n", err) } // And where config values are read once the move of config has run (issue 664): mesh-config_resolved. inv.ReadsConfigFrom(readConfigValues) // And the chain, opened when a kind that moved to the bus is first read (novox/hq ADR 0408): a command that reads none // has no use of a connection. inv.UseChainOpener(openTheChain) return inv, nil } func openIdentity(ctx context.Context) (*identity.Identity, error) { ident, err := identity.Open(ctx) if err != nil { return nil, err } if err := ident.Ready(ctx, 30*time.Second); err != nil { ident.Close() return nil, err } return ident, nil } // openLicences connects to the context that holds which model access exists and who may use it. func openLicences(ctx context.Context) (*licences.Licences, error) { held, err := licences.Open(ctx) if err != nil { return nil, err } if err := held.Ready(ctx, 30*time.Second); err != nil { held.Close() return nil, err } return held, nil } // identityOpener and licencesOpener open a store the first time a set of stores needs it; variables so a test counts // the pools opened. var ( identityOpener = openIdentity licencesOpener = openLicences ) // 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 // mu keeps one opening of each store at a time: a serving controller's stores are shared by readers that run side // by side (sharedStores). mu sync.Mutex } // 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) { h.mu.Lock() defer h.mu.Unlock() if h.identity != nil { return h.identity, nil } opened, err := identityOpener(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) { h.mu.Lock() defer h.mu.Unlock() if h.licences != nil { return h.licences, nil } opened, err := licencesOpener(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() } } // sharedStores are the stores a serving controller lends, by its inventory, to every reader in the process that holds // only the inventory (restOfTheMeshOf): one pool of each store for the controller, never one per call (issues 1945 and // 1277, 2026-10-25 16:08: about 120 take-ins opened two pools each and filled novox's store). var sharedStores sync.Map // *inventory.Inventory -> *stores // shareStores lends open by its inventory until the function returned is called. func shareStores(open *stores) func() { sharedStores.Store(open.inventory, open) return func() { sharedStores.CompareAndDelete(open.inventory, open) } } // sharedStoresOf are the stores lent for inv, if any are. func sharedStoresOf(inv *inventory.Inventory) (*stores, bool) { if open, ok := sharedStores.Load(inv); ok { return open.(*stores), true } return nil, false }