Files
mesh-host/internal/bootstrap/phase3.go

265 lines
11 KiB
Go

package bootstrap
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/novox/mesh-host/internal/declaration"
)
// Phase three — nothing is special after installation (novox/hq issue 051).
//
// Genesis raises a store and a broker before any module system exists, because the control plane
// cannot ask provisioning for the store it keeps its own records in or the broker it is reached over
// (novox/hq ADR 0006). That leaves two servers behind: the foundation's, and a second one the
// `postgres`/`lavinmq` modules used to raise for other modules to use. This turns the foundation's
// own servers into those modules, so a mesh runs ONE postgres and ONE lavinmq — the control plane's
// contexts and the database of each module that asks for one, in the same server (WBS 3.1/3.2).
//
// **Adopted in place, not replaced.** The control plane is stateless and is swapped for a fresh
// container (control.go); the store and broker hold the mesh's memory and its bus, so they are kept.
// The module declares a container with the same name, image and spec the foundation raised, and the
// applier — which keys on the container name and compares a spec digest (mesh-host internal/apply) —
// finds it already running and leaves it be. The image is pinned to the one the foundation is
// running, read from the bundle this installer produced, so the two specs are the same digest and
// nothing is recreated. A recreate happens only on a real upgrade, which is where a stated window
// belongs (WBS 3.3).
// StoreID and BrokerID are what the foundation bundle calls the two servers it raises; the modules
// that adopt them are found by these ids in the bundle this installer produced, the same way the
// control plane's own container is (ControlPlaneID).
const (
StoreID = "store"
BrokerID = "broker"
)
// InstallStore makes the foundation's store the `postgres` module, adopted in place.
//
// The order is InstallFromCatalogue's, with two additions the store needs and an ordinary provider
// does not: the server image is pinned to the one the foundation is already running (so the module's
// container is the same spec and is adopted, not a second one raised), and the superuser password —
// the foundation's, made at genesis — is carried in through `secret accept`, because the mesh cannot
// invent a credential that already created the databases (the same reasoning as the control plane's
// store connections, control.go deliverStores).
func InstallStore(ctx context.Context, o Options, control controlPlane,
foundation *declaration.Declaration, say func(string)) error {
const module = "postgres"
manifest, err := readManifest(o.Catalogue, module)
if err != nil {
return err
}
store, err := storeIn(foundation)
if err != nil {
return err
}
// The module adopts the running store rather than raising a second one, so its server container
// has to BE the foundation's — same name, same image. The applier keys on the name and compares
// a spec digest (internal/apply), so a mismatch here would not adopt the mesh's memory but
// replace it. Checked before anything is registered, so a drift between the two pinned upstream
// images (the catalogue's and the foundation bundle's) fails fast and by name, rather than
// surfacing as the mesh's store being torn down and recreated.
//
// Not rewritten to the foundation's: the server image travels through the builder, which reads
// the manifest from the repository and leaves a concrete image alone but would carry a rewrite
// nowhere. The two are kept equal at the source — one pinned postgres, named in both places.
if err := serverMatchesFoundation(manifest, store, module); err != nil {
return err
}
say(" adopting " + store.Name + " — the store the foundation raised, unchanged")
// The superuser is the foundation's, made at genesis — carried in before the push, or the push
// would seal random bytes where a working password has to be and the provisioner would not open
// the store it is meant to manage.
if err := installProvider(ctx, o, control, module, manifest, "(the provisioner; the server is adopted, not built)",
func() error {
return deliverCredential(ctx, o, control, module, "superuser", StoreSuperuserFile, "the store's superuser", say)
},
say); err != nil {
return err
}
say(" adopted mesh-store — the foundation's store is now the " + module + " module")
return nil
}
// installProvider registers, builds, issues, assigns and pushes one catalogue module, with one
// thing done just before the push — the moment a credential the mesh could not have made has to
// be in it. InstallFromCatalogue is the same sequence without that moment; the store, the broker
// and the vault each need it or need the shape, and three copies of it drifted.
func installProvider(ctx context.Context, o Options, control controlPlane, module string,
manifest []byte, buildNote string, beforePush func() error, say func(string)) error {
remote := "/" + module + "-module.json"
if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil {
return err
}
if _, err := control.tell(ctx, "module", "add", remote); err != nil {
return err
}
say(" registered " + module)
if o.CatalogSource.Repository == "" {
return fmt.Errorf("%s has to be built and there is no --catalog-source to build it from: "+
"the catalogue CHECKOUT says what it is, the catalogue REPOSITORY is where a builder "+
"clones it", module)
}
say(strings.TrimRight(" building "+module+" "+buildNote, " "))
if _, err := control.within(buildWait).tell(ctx, "build", o.CatalogSource.Repository,
"--path", "modules/"+module, "--ref", refOr(o.CatalogSource.Ref), "--wait", "1200s"); err != nil {
return err
}
if _, err := control.tell(ctx, "module", "issue", module, "--node", o.Node); err != nil {
say(" no account " + module + " — it declares nothing to say on the broker")
} else {
say(" account issued " + module)
}
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
return err
}
if err := prepareModule(ctx, o, control, module, say); err != nil {
return err
}
if beforePush != nil {
if err := beforePush(); err != nil {
return err
}
}
_, err := pushNode(ctx, o, control, say)
return err
}
// deliverCredential carries a credential genesis made into a module as its own secret, through
// `secret accept`: the mesh cannot invent the value a running server already has.
func deliverCredential(ctx context.Context, o Options, control controlPlane, module, secret, file, what string,
say func(string)) error {
value, err := readCredentialFile(file)
if err != nil {
return fmt.Errorf("%s is not at %s, so the %s module has nothing to open its server with — "+
"and the mesh cannot invent the one the server already has: %w", what, file, module, err)
}
at := "/accepting-" + secret
if err := control.carryingSecret(ctx, "mesh-accepting-"+secret, []byte(value), at); err != nil {
return err
}
if _, err := control.tell(ctx, "secret", "accept", o.Node, module, secret, "--from", at); err != nil {
return err
}
say(" accepted " + secret + " — " + what + ", as genesis made it")
return nil
}
// InstallBroker makes the foundation's broker the `lavinmq` module, adopted in place — the same
// shape as InstallStore, for the same reasons. The administrator's password is the one genesis
// gave the image's default account (rootsecrets.go), carried in so the module's provisioner can
// reach the management API as it.
func InstallBroker(ctx context.Context, o Options, control controlPlane,
foundation *declaration.Declaration, say func(string)) error {
const module = "lavinmq"
manifest, err := readManifest(o.Catalogue, module)
if err != nil {
return err
}
broker, err := brokerIn(foundation)
if err != nil {
return err
}
if err := serverMatchesFoundation(manifest, broker, module); err != nil {
return err
}
say(" adopting " + broker.Name + " — the broker the foundation raised, unchanged")
if err := installProvider(ctx, o, control, module, manifest, "(the provisioner; the server is adopted, not built)",
func() error {
return deliverCredential(ctx, o, control, module, "admin", BrokerAdminFile, "the broker's administrator", say)
},
say); err != nil {
return err
}
say(" adopted " + broker.Name + " — the foundation's broker is now the " + module + " module")
return nil
}
// InstallVault installs the vault as a foundation module (novox/hq ADR 0085, amended). Nothing to
// adopt and nothing to carry in: it is its own runtime, built from the catalogue like any provider,
// and from its first push it keeps the export of every operator-sealed secret on its own disk.
func InstallVault(ctx context.Context, o Options, control controlPlane, say func(string)) error {
const module = "mesh-vault"
manifest, err := readManifest(o.Catalogue, module)
if err != nil {
return err
}
if err := installProvider(ctx, o, control, module, manifest, "", nil, say); err != nil {
return err
}
say(" installed " + module + " — the operator-sealed export now lives on this machine, outside the store")
return nil
}
// storeIn finds the store container in the bundle this installer produced.
func storeIn(d *declaration.Declaration) (*declaration.Container, error) {
return foundationContainer(d, StoreID, "store")
}
// brokerIn finds the broker container in the bundle this installer produced.
func brokerIn(d *declaration.Declaration) (*declaration.Container, error) {
return foundationContainer(d, BrokerID, "broker")
}
func foundationContainer(d *declaration.Declaration, id, what string) (*declaration.Container, error) {
for _, r := range d.Resources {
if r.Identity() != id {
continue
}
container, ok := r.(*declaration.Container)
if !ok {
return nil, fmt.Errorf(
"this bundle's %q is a %s, not a container, so the %s module has nothing to adopt",
id, r.Kind(), what)
}
return container, nil
}
return nil, fmt.Errorf(
"this bundle names no %q, so there is no %s for a module to adopt. It declares: %s",
id, what, strings.Join(identities(d), ", "))
}
// serverMatchesFoundation checks that the module's adopting container is the one the foundation
// raised — same name, same image — so the applier reconciles it in place rather than replacing it.
func serverMatchesFoundation(manifest []byte, store *declaration.Container, module string) error {
var m struct {
Resources []struct {
Type string `json:"type"`
Name string `json:"name"`
Image string `json:"image"`
} `json:"resources"`
}
if err := json.Unmarshal(manifest, &m); err != nil {
return fmt.Errorf("the %s module's manifest is not readable: %w", module, err)
}
for _, r := range m.Resources {
if r.Type != "container" || r.Name != store.Name {
continue
}
if r.Image != store.Image {
return fmt.Errorf(
"the %s module's %q container is pinned to %q, and the foundation is running %q.\n"+
"The module adopts the foundation's store in place, so the two must name the same "+
"image — a different one would tear down the mesh's store and raise a new one on "+
"its data. Pin both to the same postgres image",
module, store.Name, r.Image, store.Image)
}
return nil
}
return fmt.Errorf(
"the %s module declares no container named %q, so it has nothing to adopt the foundation's "+
"store with. Its server container has to carry the name the foundation raised",
module, store.Name)
}