The installer carries a builder and builds the control plane it raises #9

Merged
jschoubben merged 3 commits from feat/a-module-is-a-repository-and-a-path into main 2026-09-13 09:16:51 +00:00
10 changed files with 324 additions and 42 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
/dist/ /dist/
# The placeholder `make bootstrap` moves aside while a saved image is embedded. Ignored so an # The placeholder `make bootstrap` moves aside while a saved image is embedded. Ignored so an
# interrupted release build cannot commit a twenty-megabyte tar by accident. # interrupted release build cannot commit a twenty-megabyte tar by accident.
/internal/image/control-plane.tar.placeholder /internal/image/builder.tar.placeholder
+15 -11
View File
@@ -58,13 +58,17 @@ host:
@echo "built for $(SYSTEM) carrying $(BUNDLE)" @echo "built for $(SYSTEM) carrying $(BUNDLE)"
# The installer, carrying the control plane's image: # The installer, carrying the control plane's image:
# make bootstrap IMAGE=mesh-control:v1.2.3 # make bootstrap IMAGE=mesh-builder:v1.2.3
# #
# The image is BUILT ELSEWHERE and handed over — mesh-control's own `make image` — and embedded # **The carried image is the BUILDER** (novox/hq ADR 0073). It used to be the control plane, on the
# here at release time. Not built on the machine being bootstrapped, and not fetched: the forge # argument that the forge holding the source runs on the mesh, so building at genesis would need a
# that holds mesh-control's source runs on the mesh, so a bootstrap that had to fetch or build the # mesh in order to raise one. That argument was about the *control plane's* source, and it is
# control plane would need a mesh in order to raise one. Carrying it breaks that cycle, the same # answered by ADR 0071: the source comes from a mesh that already exists, which is not the one being
# way carrying the bundle breaks the "copy it onto a machine and run it" one (novox/hq ADR 0005). # raised. What cannot be fetched is the thing that does the fetching, and that is what is carried.
#
# The image is BUILT ELSEWHERE and handed over — mesh-control's own `make builder-image` — and
# embedded here at release time, the same way carrying the bundle breaks the "copy it onto a machine
# and run it" cycle (novox/hq ADR 0005).
# #
# The saved image occupies the embed slot for the length of one build and the placeholder goes # The saved image occupies the embed slot for the length of one build and the placeholder goes
# back, exactly as `host:` does with the bundle. Nothing large is ever committed. # back, exactly as `host:` does with the bundle. Nothing large is ever committed.
@@ -84,15 +88,15 @@ host:
BOOTSTRAP_OUT ?= mesh-bootstrap BOOTSTRAP_OUT ?= mesh-bootstrap
bootstrap: bootstrap:
@test -n "$(IMAGE)" || { echo "IMAGE= is required; an installer carrying no control-plane image cannot raise a mesh"; exit 1; } @test -n "$(IMAGE)" || { echo "IMAGE= is required; an installer carrying no builder image cannot raise a mesh"; exit 1; }
@case "$(IMAGE)" in sha256:*) echo "IMAGE=$(IMAGE) is an image id. The installer identifies the carried image by its tag, because an id is the digest of a configuration that a runtime rewrites as it loads. Pass a name:tag"; exit 1;; esac @case "$(IMAGE)" in sha256:*) echo "IMAGE=$(IMAGE) is an image id. The installer identifies the carried image by its tag, because an id is the digest of a configuration that a runtime rewrites as it loads. Pass a name:tag"; exit 1;; esac
@docker image inspect "$(IMAGE)" >/dev/null 2>&1 || { echo "this machine does not hold $(IMAGE) — build it in mesh-control with 'make image'"; exit 1; } @docker image inspect "$(IMAGE)" >/dev/null 2>&1 || { echo "this machine does not hold $(IMAGE) — build it in mesh-control with 'make image'"; exit 1; }
@test -n "$$(docker image inspect --format '{{len .RepoTags}}' "$(IMAGE)" | grep -v '^0$$')" || { echo "$(IMAGE) has no repository tag, so the saved archive would carry no name the installer can ask a runtime about. Tag it first: docker tag $(IMAGE) mesh-control:<version>"; exit 1; } @test -n "$$(docker image inspect --format '{{len .RepoTags}}' "$(IMAGE)" | grep -v '^0$$')" || { echo "$(IMAGE) has no repository tag, so the saved archive would carry no name the installer can ask a runtime about. Tag it first: docker tag $(IMAGE) mesh-builder:<version>"; exit 1; }
@cp internal/image/control-plane.tar internal/image/control-plane.tar.placeholder @cp internal/image/builder.tar internal/image/builder.tar.placeholder
@docker save --output internal/image/control-plane.tar "$(IMAGE)" @docker save --output internal/image/builder.tar "$(IMAGE)"
@CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION)" -o "$(BOOTSTRAP_OUT)" ./cmd/mesh-bootstrap; \ @CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION)" -o "$(BOOTSTRAP_OUT)" ./cmd/mesh-bootstrap; \
status=$$?; \ status=$$?; \
mv internal/image/control-plane.tar.placeholder internal/image/control-plane.tar; \ mv internal/image/builder.tar.placeholder internal/image/builder.tar; \
exit $$status exit $$status
@echo "built $(BOOTSTRAP_OUT) carrying $(IMAGE)" @echo "built $(BOOTSTRAP_OUT) carrying $(IMAGE)"
+27 -10
View File
@@ -51,15 +51,16 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
version version
1 preflight what has to be true before anything is changed 1 preflight what has to be true before anything is changed
2 load the control plane's image, carried in this installer 2 load the builder's image, carried in this installer
3 bundle the substrate, named for this machine 3 build the control plane, from its own repository and a commit
4 apply raise it 4 bundle the substrate, named for this machine
5 verify it is up, and the control plane replies 5 apply raise it
6 enrol this machine becomes the mesh's first node 6 verify it is up, and the control plane replies
7 registry install the module that gives this mesh an image store 7 enrol this machine becomes the mesh's first node
8 publish push the control plane's image into it, for its first digest 8 registry install the module that gives this mesh an image store
9 control reinstall the control plane as an ordinary module, pinned to that digest 9 publish push the control plane's image into it, for its first digest
10 retire drop the temporary control plane; the host removes it 10 control reinstall the control plane as an ordinary module, pinned to that digest
11 retire drop the temporary control plane; the host removes it
--bundle the substrate template to build this machine's bundle from --bundle the substrate template to build this machine's bundle from
(default ` + defaultTemplate + `) (default ` + defaultTemplate + `)
@@ -67,8 +68,14 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
(default ` + defaultOut + `) (default ` + defaultOut + `)
--state where this node records what it has applied --state where this node records what it has applied
(default ` + store.DefaultPath + `) (default ` + store.DefaultPath + `)
--source the repository the control plane is built from, on a mesh that
already exists — not the one being raised
--source-ref the commit to build. A branch is a moving target somebody else
controls, and what is cloned here is the trust anchor for
everything this mesh will ever run
--source-path the module's directory inside that repository, if not its root
--catalog a checkout of the mesh's catalogue, holding the registry's and the --catalog a checkout of the mesh's catalogue, holding the registry's and the
control plane's manifests. Without it this stops after step 5 control plane's manifests. Without it this stops after step 6
--node the name this machine is known by (default: its hostname) --node the name this machine is known by (default: its hostname)
--registry where this mesh keeps its own images (default ` + defaultRegistry + `) --registry where this mesh keeps its own images (default ` + defaultRegistry + `)
every node pulls the control plane from this, so on a mesh of more every node pulls the control plane from this, so on a mesh of more
@@ -83,6 +90,10 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
--dry-run everything that does not change the machine --dry-run everything that does not change the machine
--json machine-readable output --json machine-readable output
The installer carries a builder, not a control plane. What raises a mesh is therefore
the same thing that will maintain it, and the control plane a mesh ends up running is
one it built itself, from a repository and a commit it can name and build again.
Genesis is a pivot: a temporary control plane installs the registry that makes it Genesis is a pivot: a temporary control plane installs the registry that makes it
permanent. The temporary one is called temp-mesh-control and the permanent one is permanent. The temporary one is called temp-mesh-control and the permanent one is
called mesh-control, so they are two containers with two owners and there is nothing called mesh-control, so they are two containers with two owners and there is nothing
@@ -175,6 +186,12 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet {
set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied") set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied")
set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue, set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue,
"a checkout of the mesh's catalogue; without it this stops after the substrate") "a checkout of the mesh's catalogue; without it this stops after the substrate")
set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository,
"the repository the control plane is built from, on a mesh that already exists")
set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref,
"the commit to build; a branch is a moving target somebody else controls")
set.StringVar(&opts.Source.Path, "source-path", opts.Source.Path,
"the module's directory inside that repository, if not its root")
set.StringVar(&opts.Node, "node", opts.Node, "the name this machine is known by") set.StringVar(&opts.Node, "node", opts.Node, "the name this machine is known by")
set.StringVar(&opts.Registry, "registry", opts.Registry, "where this mesh keeps its own images") set.StringVar(&opts.Registry, "registry", opts.Registry, "where this mesh keeps its own images")
set.StringVar(&opts.Host, "host", opts.Host, "the mesh-host binary on this machine") set.StringVar(&opts.Host, "host", opts.Host, "the mesh-host binary on this machine")
+53 -13
View File
@@ -43,6 +43,7 @@ type Step string
const ( const (
StepPreflight Step = "preflight" StepPreflight Step = "preflight"
StepLoad Step = "load" StepLoad Step = "load"
StepBuild Step = "build"
StepBundle Step = "bundle" StepBundle Step = "bundle"
StepApply Step = "apply" StepApply Step = "apply"
StepVerify Step = "verify" StepVerify Step = "verify"
@@ -53,14 +54,19 @@ const (
StepRetire Step = "retire" StepRetire Step = "retire"
) )
// Steps in the order they happen, so a failure can say "step 2 of 10". // Steps in the order they happen, so a failure can say "step 2 of 11".
// //
// The first five make a machine; the last five make a mesh that can maintain itself. They are one // The first six make a machine; the last five make a mesh that can maintain itself. They are one
// program because they are one procedure — the whole reason the pivot exists is that steps 7 to 9 // program because they are one procedure — the whole reason the pivot exists is that steps 8 to 10
// cannot happen without steps 1 to 5, and steps 1 to 5 leave something that cannot be upgraded // cannot happen without steps 1 to 6, and steps 1 to 6 leave something that cannot be upgraded
// without steps 7 to 9 (novox/hq ADR 0067). // without steps 8 to 10 (novox/hq ADR 0067).
//
// **Build sits between load and bundle**, because the bundle has to name an image and that image
// no longer arrives finished. The installer carries the builder, loads it, and uses it to produce
// the control plane from source (novox/hq ADR 0073) — so what the bundle names is something this
// mesh made, out of a repository and a commit it can name, and can therefore make again.
var Steps = []Step{ var Steps = []Step{
StepPreflight, StepLoad, StepBundle, StepApply, StepVerify, StepPreflight, StepLoad, StepBuild, StepBundle, StepApply, StepVerify,
StepEnrol, StepRegistry, StepPublish, StepControlPlane, StepRetire, StepEnrol, StepRegistry, StepPublish, StepControlPlane, StepRetire,
} }
@@ -118,6 +124,11 @@ type Options struct {
// looks installed and cannot upgrade itself. // looks installed and cannot upgrade itself.
Catalogue string Catalogue string
// Source is where the control plane is built from — a repository on a mesh that already
// exists, and a commit. The installer carries the builder rather than a finished control
// plane (novox/hq ADR 0073), so this is what it is told to make.
Source Source
// Registry is where this mesh's own images live, as this machine reaches it. Every node will // Registry is where this mesh's own images live, as this machine reaches it. Every node will
// pull the control plane from what this says, so on a mesh of more than one machine it must be // pull the control plane from what this says, so on a mesh of more than one machine it must be
// an address the others can reach. // an address the others can reach.
@@ -171,6 +182,14 @@ type Result struct {
ImageTags []string `json:"image-tags,omitempty"` ImageTags []string `json:"image-tags,omitempty"`
// ImageHeld is true when the machine already held it and nothing was loaded. // ImageHeld is true when the machine already held it and nothing was loaded.
ImageHeld bool `json:"image-already-held,omitempty"` ImageHeld bool `json:"image-already-held,omitempty"`
// Built is what the genesis build produced, and BuiltFrom is the commit it actually built.
//
// Reported because they are the difference between a mesh that can rebuild its control plane
// and one that cannot: a machine holding these can be asked for the same thing again and get
// the same thing back.
Built string `json:"built,omitempty"`
BuiltFrom string `json:"built-from,omitempty"`
// ImagePredicted is true when Image is the archive's id because nothing was loaded — a dry run // ImagePredicted is true when Image is the archive's id because nothing was loaded — a dry run
// only, and the reason a dry run does not claim to know what would be applied. // only, and the reason a dry run does not claim to know what would be applied.
ImagePredicted bool `json:"image-id-is-a-prediction,omitempty"` ImagePredicted bool `json:"image-id-is-a-prediction,omitempty"`
@@ -287,18 +306,34 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
say(" system " + sys.Name()) say(" system " + sys.Name())
// ---- 2. load ------------------------------------------------------------------------ // ---- 2. load ------------------------------------------------------------------------
say("load — the control plane's image, carried in this installer") say("load — the builder's image, carried in this installer")
loaded, err := Load(ctx, d.Run, o.DryRun, say) loaded, err := Load(ctx, d.Run, o.DryRun, say)
if err != nil { if err != nil {
return result, failed(StepLoad, err) return result, failed(StepLoad, err)
} }
// These describe the CARRIED image, which is the builder. What the control plane ends up
// being is reported separately, by the build below.
result.Image, result.ImageTags, result.ImageHeld = loaded.ID, loaded.Tags, loaded.Held result.Image, result.ImageTags, result.ImageHeld = loaded.ID, loaded.Tags, loaded.Held
result.ImageArchive, result.ImageTag = loaded.Archive, loaded.Tag result.ImageArchive, result.ImageTag = loaded.Archive, loaded.Tag
result.ImagePredicted = loaded.Predicted result.ImagePredicted = loaded.Predicted
// ---- 3. bundle ---------------------------------------------------------------------- // ---- 3. build -----------------------------------------------------------------------
say("build — the control plane, from its own repository and a commit")
built, err := BuildControlPlane(ctx, d.Run, loaded.Tag, o.Source, o.DryRun, say)
if err != nil {
return result, failed(StepBuild, err)
}
result.Built, result.BuiltFrom = built.Module, built.Commit
controlPlaneImage := built.Image
if o.DryRun {
// Nothing was built, so there is no id to name. The carried builder's own is used only so
// the remaining steps have something well-formed to describe; nothing is applied.
controlPlaneImage = loaded.ID
}
// ---- 4. bundle ----------------------------------------------------------------------
say("bundle — what this machine will be asked to be") say("bundle — what this machine will be asked to be")
rewritten, err := Rewrite(template, loaded.ID) rewritten, err := Rewrite(template, controlPlaneImage)
if err != nil { if err != nil {
return result, failed(StepBundle, err) return result, failed(StepBundle, err)
} }
@@ -369,9 +404,9 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
// inside a pull that cannot succeed, three steps from the cause. // inside a pull that cannot succeed, three steps from the cause.
if loaded.Predicted { if loaded.Predicted {
return result, failed(StepBundle, fmt.Errorf( return result, failed(StepBundle, fmt.Errorf(
"the control plane's image id was never confirmed against this machine's runtime, and "+ "the builder's image id was never confirmed against this machine's runtime, and the "+
"the bundle was about to be written with it. This is a fault in the installer, not "+ "build was about to be run with it. This is a fault in the installer, not in the "+
"in the machine")) "machine"))
} }
if err := writeBundleFile(o.Out, rewritten.Bundle); err != nil { if err := writeBundleFile(o.Out, rewritten.Bundle); err != nil {
@@ -444,7 +479,12 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
// ---- 8. publish ----------------------------------------------------------------------- // ---- 8. publish -----------------------------------------------------------------------
say("publish — the control plane's image gets its first manifest digest") say("publish — the control plane's image gets its first manifest digest")
published, err := PublishControlPlane(ctx, o, d, loaded.ID, say) // **The image this mesh built, not the one the installer carried.** The carried one is the
// builder; publishing it here would put a builder in the registry under the control plane's
// name and install it as the control plane — which is exactly what happened the first time
// this ran, and presented as a control plane that started, printed a builder's usage, and
// exited cleanly over and over.
published, err := PublishControlPlane(ctx, o, d, controlPlaneImage, say)
result.PublishedAs, result.PublishedAlready = published.Reference, published.Already result.PublishedAs, result.PublishedAlready = published.Reference, published.Already
if err != nil { if err != nil {
return result, failed(StepPublish, err) return result, failed(StepPublish, err)
+165
View File
@@ -0,0 +1,165 @@
package bootstrap
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Source is where the control plane is built from.
//
// **A commit, not a branch** (novox/hq ADR 0071). The forge a mesh installs from is the trust
// anchor for everything that mesh will ever run, and a branch is a moving target somebody else
// controls. The installer names what it wants and the builder checks what it got.
type Source struct {
// Repository is the clone URL, on a mesh that already exists. Not the one being raised.
Repository string
// Ref is the commit to build.
Ref string
// Path is the module's directory inside that repository. Empty is its root.
Path string
}
// Named reports whether a source was given at all.
func (s Source) Named() bool { return strings.TrimSpace(s.Repository) != "" }
// Check is whether this installer was told enough to build anything.
//
// **Its own function so it can be asked twice**: once in preflight, before the machine has been
// touched, and once at the build, which is where it would otherwise be discovered. The first is
// what a person wants — a run that cannot finish should say so before it changes anything — and
// the second is what keeps the build honest if it is ever called from somewhere else.
func (s Source) Check() error {
if !s.Named() {
return errors.New(
"this installer carries a builder and was not told what to build. Give it --source " +
"(a repository on a mesh that already exists) and --source-ref (a commit). It " +
"does not guess: what it clones is the trust anchor for everything this mesh " +
"will ever run")
}
if strings.TrimSpace(s.Ref) == "" {
return errors.New(
"--source was given without --source-ref. Genesis names a commit, because a branch " +
"is a moving target somebody else controls and what is cloned here is the trust " +
"anchor for everything this mesh will ever run (novox/hq ADR 0071)")
}
return nil
}
// Built is what one genesis build produced.
type Built struct {
// Module is what the manifest called itself, so the installer can say it built the right thing.
Module string
// Commit is what was actually built, which may not be what was asked for if a ref moved.
Commit string
// Image is the artifact, named by the digest of its own configuration — the identity a machine
// can use with nothing serving it, and the same one the installer used for a carried image.
Image string
}
// builderOutput is the part of the builder's one-shot result this needs.
type builderOutput struct {
Module string `json:"module"`
Commit string `json:"commit"`
Made []struct {
Name string `json:"name"`
Kind string `json:"kind"`
Reference string `json:"reference"`
} `json:"made"`
}
// BuildControlPlane runs the carried builder once, to produce the control plane from source.
//
// **This is the step that makes a raised mesh able to maintain itself** (novox/hq ADR 0073). What
// comes out is not merely an image: it came from a named repository, a path and a commit, which is
// the same description every later rebuild of the control plane will use. A mesh raised this way
// can rebuild the thing that runs it. A mesh handed a finished image cannot, and has no way to
// discover that until somebody needs it to.
//
// The builder is given the machine's container runtime and nothing else. It is not given a registry:
// there is none yet, and none is needed — the image it produces stays in the runtime of the machine
// that will run it, which is this one.
func BuildControlPlane(ctx context.Context, run Runner, builderTag string, source Source,
dryRun bool, say func(string)) (Built, error) {
if err := source.Check(); err != nil {
return Built{}, err
}
args := []string{
"run", "--rm",
// The build runs containers of its own, which is the whole of what it needs.
"-v", "/var/run/docker.sock:/var/run/docker.sock",
builderTag,
"build", source.Repository, "--ref", source.Ref,
}
if source.Path != "" {
args = append(args, "--path", source.Path)
}
say(fmt.Sprintf("building the control plane from %s at %s", source.Repository, shortRef(source.Ref)))
if dryRun {
say(" dry run: not built")
return Built{}, nil
}
out, err := run(ctx, "docker", args...)
if err != nil {
return Built{}, fmt.Errorf("the control plane could not be built from %s at %s: %w",
source.Repository, shortRef(source.Ref), err)
}
// The builder writes its result to standard output and everything else to standard error, so
// what is parsed here is the whole of what it said. Trimmed rather than searched: a parser that
// hunts for the first `{` will happily read a brace out of a progress line.
var result builderOutput
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &result); err != nil {
return Built{}, fmt.Errorf(
"the builder finished and what it said is not a result: %w. What it said was: %s",
err, firstLine(out))
}
var images []string
for _, made := range result.Made {
if made.Kind == "image" {
images = append(images, made.Reference)
}
}
switch len(images) {
case 1:
case 0:
return Built{}, fmt.Errorf(
"%s built, and produced no image. The installer raises the control plane from an "+
"image, so there is nothing here to raise", result.Module)
default:
// Refused rather than guessed at. Picking one of several would work until the day the
// order changed, and then raise the wrong thing without saying so.
return Built{}, fmt.Errorf(
"%s produced %d images, and the installer cannot tell which one is the control "+
"plane. A module raised at genesis declares exactly one",
result.Module, len(images))
}
say(fmt.Sprintf(" built %s from %s", result.Module, shortRef(result.Commit)))
return Built{Module: result.Module, Commit: result.Commit, Image: images[0]}, nil
}
func shortRef(ref string) string {
if len(ref) > 8 {
return ref[:8]
}
return ref
}
func firstLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
if len(s) > 200 {
return s[:200]
}
return s
}
+41
View File
@@ -0,0 +1,41 @@
package bootstrap
import (
"strings"
"testing"
)
// An installer that carries a builder and was told nothing refuses, and says what is missing.
//
// **Exercised rather than assumed.** This is the refusal that stands between a person and a
// machine left holding a store, a broker and no control plane — the failure the whole preflight
// exists to prevent — and a refusal nothing tests is a refusal nobody has read.
func TestAnInstallerWithNothingToBuildRefuses(t *testing.T) {
err := Source{}.Check()
if err == nil {
t.Fatal("a source naming no repository was accepted; nothing would have been built")
}
for _, want := range []string{"--source", "--source-ref"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not name %s, so it does not say how to fix it: %v", want, err)
}
}
}
// A repository without a commit refuses too, because a branch is somebody else's moving target.
func TestABranchIsNotACommit(t *testing.T) {
err := Source{Repository: "https://example.invalid/mesh-control.git"}.Check()
if err == nil {
t.Fatal("a source with no ref was accepted; genesis would have built whatever a branch pointed at")
}
if !strings.Contains(err.Error(), "--source-ref") {
t.Errorf("the refusal does not name the flag that fixes it: %v", err)
}
}
// And a repository with a commit is enough.
func TestARepositoryAndACommitIsEnough(t *testing.T) {
if err := (Source{Repository: "https://example.invalid/mesh-control.git", Ref: "a1b2c3d4"}).Check(); err != nil {
t.Fatalf("a repository and a commit were refused: %v", err)
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ type Loaded struct {
Predicted bool Predicted bool
} }
// Load puts the carried control-plane image into this machine's container runtime, and reports // Load puts the carried builder image into this machine's container runtime, and reports
// what the runtime decided to call it. // what the runtime decided to call it.
// //
// **The digest of a configuration is not portable across runtimes, and that is why the id is read // **The digest of a configuration is not portable across runtimes, and that is why the id is read
+10 -1
View File
@@ -32,6 +32,15 @@ func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte
if image.IsEmpty() { if image.IsEmpty() {
return nil, image.ErrEmpty return nil, image.ErrEmpty
} }
// And was it told what to build?
//
// Asked here rather than at the build, for the same reason as the line above: a run that
// cannot finish should say so before it has changed anything. The installer carries a builder
// and nothing else (novox/hq ADR 0073), so an installer with no source is an installer that
// would raise a store and a broker and then have nothing to raise a control plane from.
if err := o.Source.Check(); err != nil {
return nil, fmt.Errorf("%w. Nothing has been changed on this machine", err)
}
saved, err := image.Saved() saved, err := image.Saved()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -56,7 +65,7 @@ func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte
"Rebuild the installer with a tagged image: `make bootstrap IMAGE=<name>:<tag>`", "Rebuild the installer with a tagged image: `make bootstrap IMAGE=<name>:<tag>`",
carriedID) carriedID)
} }
say(fmt.Sprintf(" control plane %s carried (the archive calls it %s)", tag, carriedID)) say(fmt.Sprintf(" builder %s carried (the archive calls it %s)", tag, carriedID))
// 2. Is the template there, and is it a substrate? // 2. Is the template there, and is it a substrate?
template, err := os.ReadFile(o.Template) template, err := os.ReadFile(o.Template)
+11 -5
View File
@@ -30,6 +30,12 @@ import (
// The saved image, replaced at release time by `make bootstrap`. // The saved image, replaced at release time by `make bootstrap`.
// //
// **It is the builder, not the control plane** (novox/hq ADR 0073). The installer used to carry
// the thing it was going to run; it now carries the thing that makes it. One artifact either way —
// but a mesh raised by the second one holds a control plane it built from source, out of the same
// repository and path every later rebuild of it will use, and can therefore rebuild it. A mesh
// raised by the first held an artifact it could not reproduce and knew nothing about.
//
// What is committed here is a placeholder, for the same reason `internal/bundle` commits locks // What is committed here is a placeholder, for the same reason `internal/bundle` commits locks
// that are only comments: `go:embed` refuses to compile against a file that is not there, so a // that are only comments: `go:embed` refuses to compile against a file that is not there, so a
// checkout with nothing embedded would not build at all — and someone reading this repository or // checkout with nothing embedded would not build at all — and someone reading this repository or
@@ -41,18 +47,18 @@ import (
// the length of one build and then puts the placeholder back — exactly what `make host` does with // the length of one build and then puts the placeholder back — exactly what `make host` does with
// the bundle it embeds. // the bundle it embeds.
// //
//go:embed control-plane.tar //go:embed builder.tar
var saved []byte var saved []byte
// ErrEmpty means this installer carries no control-plane image. // ErrEmpty means this installer carries no builder image.
// //
// A separate error rather than a message, so the caller can refuse in preflight — before a // A separate error rather than a message, so the caller can refuse in preflight — before a
// machine has been touched — instead of discovering it at the load, after the runtime has been // machine has been touched — instead of discovering it at the load, after the runtime has been
// probed and a bundle has been written. // probed and a bundle has been written.
var ErrEmpty = errors.New( var ErrEmpty = errors.New(
"this mesh-bootstrap carries no control-plane image, so it cannot raise a mesh. A release " + "this mesh-bootstrap carries no builder image, so it cannot raise a mesh. A release build " +
"build embeds one: `make bootstrap IMAGE=<image>` in the mesh-host repository, where " + "embeds one: `make bootstrap IMAGE=<image>` in the mesh-host repository, where <image> " +
"<image> is a control-plane image already built from the mesh-control source") "is a mesh-builder image already built from the mesh-control source")
// IsEmpty reports whether anything was built in. // IsEmpty reports whether anything was built in.
// //