Files
jschoubben c4ce57997e The packages port given at genesis is a module's setting, like every other
Every foundation port given at genesis became a per-node setting of the module
that binds it, except the package registry's: that one was fixed by rewriting
the builder's manifest when the installer registered it. Registering the builder
again from the catalogue undid it, and the forge's own module, when it took the
bootstrap forge over, came up on the catalogue's port — which on a machine where
a predecessor holds 3000 points the builder at the predecessor's forge.

So the rewrite is gone, and the port is recorded twice as a setting, both from
the one input:

- the forge's module is registered at genesis — not assigned, nothing of it runs
  — so the controller has something to hold `{"ports": {"3000": <given>}}`
  against. Assigning the forge later raises it on the port this machine was
  given, and its container, its filter rule, its opening, what it serves and
  what consumers are told all read it from there.
- the builder is given `{"serves": {"port": <given>}}`, which merges into the
  binding it carries in place of one nothing can resolve yet.

A genesis on the catalogue's port records nothing and registers nothing, so it
does exactly what it did before.

novox/hq 04-ISSUES/085, ADR 0100
2026-09-22 21:40:02 +02:00

87 lines
3.5 KiB
Go

package bootstrap
import (
"context"
"fmt"
"strings"
)
// BuilderModule is the module that lets a mesh produce anything at all.
const BuilderModule = "builder"
// BuilderRepository is what its image is called in this mesh's own registry.
const BuilderRepository = "mesh-builder"
// Builder is what installing it produced.
type Builder struct {
Published Published
Installed Installed
// Account is true when a broker account was issued for it here.
Account bool
}
// InstallBuilder gives a fresh mesh the thing that makes everything else.
//
// **Without this a mesh can run and cannot produce** (novox/hq ADR 0073). Genesis ends with a
// control plane, a store, a queue and a registry — and almost every module in the catalogue is
// waiting to be built, because a manifest names artifacts and nothing has made them. The builder is
// one of the few things that cannot be built by the thing it is, so it is carried; and it is
// already here, because it is what built the control plane.
//
// So this is the same two acts the control plane went through, in the same order and for the same
// reason: publish the image so the mesh names it by a digest its own registry assigned rather than
// by a local identity nothing else can fetch, then install it as an ordinary module pinned to that.
//
// And then the part only it needs: a broker account. A builder takes work from a queue and
// announces what it made, and it holds its own credential for that like any module — asking for a
// generic one produced an account that could do neither, which is what made this worth its own step
// rather than a line in another.
func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane, imageID string,
say func(string)) (Builder, error) {
var out Builder
published, err := publishAs(ctx, o, d, imageID, BuilderRepository, say)
out.Published = published
if err != nil {
return out, err
}
manifest, err := readManifest(o.Catalogue, BuilderModule)
if err != nil {
return out, fmt.Errorf("%w\n"+
"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 := pinPlaceholder(manifest, published.Reference, BuilderModule)
if err != nil {
return out, err
}
say(fmt.Sprintf(" pinned to %s, named in %d place(s)", published.Reference, places))
installed, err := registerAndAssign(ctx, o, control, BuilderModule, pinned, say)
out.Installed = installed
if err != nil {
return out, err
}
// Before the push, so the account is in the declaration the machine receives rather than in
// the one after it. A builder that arrives without its credential starts, finds nothing it may
// read, and waits — which looks exactly like a builder with no work.
account := o.Node + "-" + BuilderModule
if _, err := control.tell(ctx, "builder", "issue", account, "--node", o.Node); err != nil {
// Said and carried on. An account that already exists is the ordinary case on a re-run,
// and the push below is what makes either state true on the machine.
if !strings.Contains(err.Error(), "already") {
return out, fmt.Errorf("the builder has no broker account, so it can take no work: %w", err)
}
say(" broker account " + account + " — already issued")
} else {
out.Account = true
say(" broker account " + account + ", scoped to what it consumes and emits")
}
out.Installed.Pushed, err = pushNode(ctx, o, control, say)
return out, err
}