Phase 3.1: adopt the foundation store as the postgres module
InstallStore turns the mesh-store the foundation raised at genesis into the postgres module, adopted in place: it verifies the module's server names the same container and the same image the foundation is running (fail-fast on a drift, rather than tearing down the mesh's store), then registers, builds the provisioner, and carries the superuser in via secret accept — the mesh cannot invent a credential that already made the databases (mirroring the control plane's store-connection delivery, control.go). pinImage generalised to any module for reuse. Issue 051 (WBS 3.1). One server holds the controller's contexts and every module's database. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -588,10 +588,12 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
}
|
||||
|
||||
// ---- 14. store ------------------------------------------------------------------------
|
||||
// A database PROVIDER. The foundation's store is the control plane's own memory and offers
|
||||
// nothing to anything; the first thing that wants a database is the catalogue, next.
|
||||
say("store — a database provider, which the foundation's own store is not")
|
||||
if err := InstallFromCatalogue(ctx, o, permanentControl, "postgres", say); err != nil {
|
||||
// The foundation's own store, ADOPTED as the `postgres` module (novox/hq issue 051): one
|
||||
// server holds the control plane's contexts and every module's database, rather than the
|
||||
// foundation's store beside a second one a module raised. Adopted in place — the module names
|
||||
// the container the foundation is already running, and the applier leaves it be (phase3.go).
|
||||
say("store — the foundation's store, adopted as the postgres module: one server, not two")
|
||||
if err := InstallStore(ctx, o, permanentControl, rewritten.Declaration, say); err != nil {
|
||||
return result, failed(StepStore, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane
|
||||
"This is the manifest that makes the builder an ordinary module. Without it the mesh "+
|
||||
"has the image and no way to run it, so nothing can be built here", err)
|
||||
}
|
||||
pinned, places, err := pinImage(manifest, published.Reference)
|
||||
pinned, places, err := pinImage(manifest, published.Reference, BuilderModule)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
|
||||
"have pivoted", err)
|
||||
}
|
||||
|
||||
pinned, places, err := pinImage(manifest, image)
|
||||
pinned, places, err := pinImage(manifest, image, ControlPlaneModule)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -130,15 +130,15 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
|
||||
// carrying a real digest is one somebody pinned by hand, and quietly registering it would install a
|
||||
// control plane that is not the image this machine just published — which is the one thing this
|
||||
// step exists to guarantee.
|
||||
func pinImage(manifest []byte, reference string) ([]byte, int, error) {
|
||||
func pinImage(manifest []byte, reference, module string) ([]byte, int, error) {
|
||||
places := bytes.Count(manifest, []byte(placeholderDigest))
|
||||
if places == 0 {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"the %s module's manifest carries no placeholder digest (%s), so there is nothing to "+
|
||||
"pin to the image this machine just published.\n"+
|
||||
"A manifest already naming a digest was pinned by somebody else, to some other "+
|
||||
"build. Registering it would install a control plane that is not the one this "+
|
||||
"installer carried and pushed", ControlPlaneModule, placeholderDigest)
|
||||
"build. Registering it would install a module that is not the one this "+
|
||||
"installer carried and pushed", module, placeholderDigest)
|
||||
}
|
||||
// The reference the registry gave back is `<registry>/<repository>@sha256:…`, and what the
|
||||
// manifest holds is `<something>@sha256:0…0`. Replacing only the digest would leave the
|
||||
@@ -157,7 +157,7 @@ func pinImage(manifest []byte, reference string) ([]byte, int, error) {
|
||||
if start < 0 {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"the %s module's manifest has a placeholder digest that is not inside a JSON "+
|
||||
"string, so the installer cannot tell what image it belongs to", ControlPlaneModule)
|
||||
"string, so the installer cannot tell what image it belongs to", module)
|
||||
}
|
||||
out.Write(rest[:start+1])
|
||||
out.WriteString(reference)
|
||||
@@ -170,12 +170,12 @@ func pinImage(manifest []byte, reference string) ([]byte, int, error) {
|
||||
var checked map[string]any
|
||||
if err := json.Unmarshal(pinned, &checked); err != nil {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"pinning the %s module's image broke its manifest: %w", ControlPlaneModule, err)
|
||||
"pinning the %s module's image broke its manifest: %w", module, err)
|
||||
}
|
||||
if bytes.Contains(pinned, []byte(placeholderDigest)) {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"the %s module's manifest still carries a placeholder digest after pinning",
|
||||
ControlPlaneModule)
|
||||
module)
|
||||
}
|
||||
return pinned, places, nil
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const pushedReference = "127.0.0.1:5000/mesh-controller@sha256:" +
|
||||
// with no registry in front — which a runtime would go to the internet for, and this mesh's
|
||||
// control plane exists in no public registry by design.
|
||||
func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
|
||||
pinned, places, err := pinImage([]byte(theControlPlaneModule), pushedReference)
|
||||
pinned, places, err := pinImage([]byte(theControlPlaneModule), pushedReference, "mesh-controller")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
|
||||
func TestAManifestAlreadyPinnedByHandIsRefused(t *testing.T) {
|
||||
already := strings.Replace(theControlPlaneModule, placeholderDigest,
|
||||
"sha256:"+strings.Repeat("9", 64), 1)
|
||||
if _, _, err := pinImage([]byte(already), pushedReference); err == nil {
|
||||
if _, _, err := pinImage([]byte(already), pushedReference, "mesh-controller"); err == nil {
|
||||
t.Fatal("a manifest already pinned to some other image was accepted")
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
|
||||
{"id": "migrate", "type": "container", "name": "mesh-controller-migrate", "run-once": true,
|
||||
"image": "mesh-controller@`+placeholderDigest+`", "args": ["migrate"]},`, 1)
|
||||
|
||||
pinned, places, err := pinImage([]byte(twice), pushedReference)
|
||||
pinned, places, err := pinImage([]byte(twice), pushedReference, "mesh-controller")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
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 every module's database 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
|
||||
}
|
||||
|
||||
// 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"
|
||||
value := strings.TrimSpace(store.Env["POSTGRES_PASSWORD"])
|
||||
if value == "" {
|
||||
return fmt.Errorf(
|
||||
"the foundation's store names no POSTGRES_PASSWORD, so the %s module has no superuser "+
|
||||
"to open it with — and the mesh cannot invent the one that already made the databases",
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user