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