Files
mesh-controller/cmd/mesh-controller/stores.go
T
jochen 18da8b37e9 Retire the networking bundle and what the control plane stops shipping (hq ADR 0226)
networking required mesh-wireguard and nothing else; machines are assigned the network directly.
module forget refuses a provided module, so a retired one is removed at start once no machine has it.
Guard route-proxy's public account directory against a reissue.
2026-10-06 14:59:28 +02:00

218 lines
6.9 KiB
Go

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)
}
// 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)
}
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()
}
}