114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"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)
|
|
}
|
|
if manifest, err = followPackagesPort(manifest, o.Ports.orDefaults().Packages); err != nil {
|
|
return out, 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
|
|
}
|
|
|
|
// packagesPortInBinding is the package registry's port as the builder's manifest names it, in the
|
|
// binding file it carries — JSON inside a JSON string, so its quotes are escaped.
|
|
const packagesPortInBinding = `\"port\": 3000`
|
|
|
|
// followPackagesPort points the builder's package binding at the port the node gave the package
|
|
// registry (novox/hq ADR 0100). Genesis raises the registry by hand before gitea is a module, so no
|
|
// binding the controller resolves can say where it is; the builder carries the address in its own
|
|
// manifest, and a port given at genesis must reach it there or the base build dials a port nothing
|
|
// answers on. At the default it is left byte for byte as the catalogue has it.
|
|
func followPackagesPort(manifest []byte, port int) ([]byte, error) {
|
|
if port == defaultGiteaPort {
|
|
return manifest, nil
|
|
}
|
|
if n := bytes.Count(manifest, []byte(packagesPortInBinding)); n != 1 {
|
|
return nil, fmt.Errorf("the builder's manifest names the package registry's port %d time(s) where "+
|
|
"this installer looks for it once (%s), so the port given with --packages-port cannot reach "+
|
|
"it; nothing was changed", n, packagesPortInBinding)
|
|
}
|
|
return bytes.Replace(manifest, []byte(packagesPortInBinding),
|
|
[]byte(`\"port\": `+strconv.Itoa(port)), 1), nil
|
|
}
|