One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
179 lines
5.1 KiB
Go
179 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"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)
|
|
}
|
|
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()
|
|
}
|
|
}
|