package main import ( "context" "fmt" "time" "github.com/novox/mesh-controller/internal/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) } // 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 } // 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) } 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 } // 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() } }