package bootstrap import ( "context" "encoding/json" "fmt" "os" "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") 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(" building " + module + " (the provisioner; the server is adopted, not built)") 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 } // 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 := deliverSuperuser(ctx, o, control, module, store, say); err != nil { return err } if _, err := pushNode(ctx, o, control, say); err != nil { return err } say(" adopted mesh-store — the foundation's store is now the " + module + " module") return nil } func readCredentialFile(path string) (string, error) { raw, err := os.ReadFile(path) if err != nil { return "", err } value := strings.TrimRight(string(raw), "\r\n") if value == "" { return "", fmt.Errorf("%s is empty", path) } return value, 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 through `secret accept` 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") 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", module) } say(" building " + module + " (the provisioner; the server is adopted, not built)") 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 } value, err := readCredentialFile(BrokerAdminFile) if err != nil { return fmt.Errorf("the broker's administrator password genesis made is not at %s: %w", BrokerAdminFile, err) } at := "/accepting-admin" if err := control.carrying(ctx, "mesh-accepting-admin", []byte(value), at); err != nil { return err } if _, err := control.tell(ctx, "secret", "accept", o.Node, module, "admin", "--from", at); err != nil { return err } say(" accepted admin — the broker's administrator, as genesis made it") if _, err := pushNode(ctx, o, control, 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: 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 } 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", module) } say(" building " + module) 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 { return err } say(" account issued " + module) if _, err := control.tell(ctx, "assign", o.Node, module); err != nil { return err } if _, err := pushNode(ctx, o, control, 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) } // deliverSuperuser carries the store's superuser password into the module. // // It is the foundation's, set on the bundle's store container at genesis; the mesh cannot invent a // credential that already made the databases, so it goes in through `secret accept`, exactly as the // control plane's store connections do (control.go deliverStores). func deliverSuperuser(ctx context.Context, o Options, control controlPlane, module string, store *declaration.Container, say func(string)) error { const secret = "superuser" // Genesis made it and kept it in the file the store was raised from (rootsecrets.go); the // template's environment variable is accepted too, for a bundle produced before that. value, err := readCredentialFile(StoreSuperuserFile) if err != nil { value = strings.TrimSpace(store.Env["POSTGRES_PASSWORD"]) } if value == "" { return fmt.Errorf( "neither %s nor the foundation's store names the superuser password, so the %s module "+ "has nothing to open the store with — and the mesh cannot invent the one that already "+ "made the databases", StoreSuperuserFile, module) } at := "/accepting-" + secret if err := control.carrying(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 + " — the store's superuser, as the foundation made it") return nil }