The first real run of mesh-bootstrap stopped in preflight, dialling 192.0.2.250:5000 for ninety seconds on a machine whose network was fine. That address is the registry the lab used to raise; the substrate template still names the control plane by it, and step 3 replaces that reference with the id of the image this installer carries. Nothing ever pulls it. So preflight excludes the control plane's resource by identity, rather than by the happy accident of the template filling its slot with something that needs no registry. Every other container's registry is still dialled, because those are somebody else's images at somebody else's registry and a machine that cannot reach one fails inside a pull, which says the wrong thing. Also: `make bootstrap` takes BOOTSTRAP_OUT. The lab now builds the installer from source before every raise, into a path it chooses, and a caller that could not say where the output goes would have to copy it afterwards. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
241 lines
10 KiB
Go
241 lines
10 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/image"
|
|
"github.com/novox/mesh-host/internal/profile"
|
|
)
|
|
|
|
// DefaultRegistry is where an image reference that names no host comes from.
|
|
const DefaultRegistry = "registry-1.docker.io:443"
|
|
|
|
// Preflight refuses early and plainly, and returns the bundle template it read.
|
|
//
|
|
// Everything here is a thing that will otherwise be discovered half way through: a machine with
|
|
// no runtime found after a bundle has been written, a template that does not parse found after an
|
|
// image has been loaded, a registry that cannot be reached found inside a `docker pull` that
|
|
// reports a network error and not a missing image. The order is cheapest first, so a mistake in
|
|
// what the installer was pointed at costs nothing to find.
|
|
func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte, error) {
|
|
// 1. Does this installer carry what it claims to?
|
|
//
|
|
// Asked before the machine is touched, for the same reason `mesh-host bundle` exists: a host
|
|
// that carries no substrate must say so when somebody asks, not on a first node
|
|
// (internal/bundle). An installer built without an image would otherwise get a machine as far
|
|
// as a running store and a running broker and stop.
|
|
if image.IsEmpty() {
|
|
return nil, image.ErrEmpty
|
|
}
|
|
saved, err := image.Saved()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
// The id said here is the ARCHIVE's, and it is reported as such: it is a fact about the file
|
|
// and not about this machine. What this runtime will call the image once it holds it is the
|
|
// runtime's decision, made at the load, and asked for there (see Load).
|
|
carriedID, err := image.ArchiveID(saved)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tag := firstOr(image.Tags(saved), "")
|
|
if tag == "" {
|
|
// Refused here as well as at the load, because preflight's whole job is to find at the
|
|
// start what would otherwise be found half way through — and this would be found after an
|
|
// image had been written to disk and handed to a container runtime.
|
|
return nil, fmt.Errorf(
|
|
"the carried control-plane image has no tag, and the installer identifies it by one: "+
|
|
"an image id is the digest of a configuration that a runtime rewrites as it loads, "+
|
|
"so the archive's id (%s) is not necessarily the id this machine would hold.\n"+
|
|
"Rebuild the installer with a tagged image: `make bootstrap IMAGE=<name>:<tag>`",
|
|
carriedID)
|
|
}
|
|
say(fmt.Sprintf(" control plane %s carried (the archive calls it %s)", tag, carriedID))
|
|
|
|
// 2. Is the template there, and is it a substrate?
|
|
template, err := os.ReadFile(o.Template)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"the bundle template could not be read: %w\n"+
|
|
"It is what this machine will be asked to be, so there is nothing to do without "+
|
|
"it. Point --bundle at one; mesh-host's examples/substrate-first-node.lock is "+
|
|
"the shape", err)
|
|
}
|
|
// Parsed here as well as at the rewrite, because a template that is not a declaration should
|
|
// cost a second rather than an image load and a written file.
|
|
parsed, err := declaration.ParseFileTrusted(template)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the bundle template is not a declaration: %w", err)
|
|
}
|
|
if _, err := controlPlaneIn(parsed); err != nil {
|
|
return nil, err
|
|
}
|
|
say(fmt.Sprintf(" bundle template %s (%d resources)", o.Template, len(parsed.Resources)))
|
|
|
|
// 3. Does a container runtime ANSWER?
|
|
//
|
|
// Not "is it installed" — novox/hq 04-ISSUES/007 is exactly that mistake, and the detector
|
|
// this uses is the one written for it: it asks the daemon for its server version, which fails
|
|
// when the daemon is down however complete the installation is.
|
|
//
|
|
// **Yes, the bundle installs the runtime itself**, and that is not a contradiction. The
|
|
// installer needs one BEFORE the apply, because the control plane's image is loaded into it
|
|
// first; the bundle still declares the package and the service because the host must own them
|
|
// and reassert them at every reconcile. So this is not a duplicate check — it is the one thing
|
|
// the bootstrap cannot bootstrap.
|
|
//
|
|
// Polled rather than asked once. A socket-activated daemon queued behind
|
|
// `network-online.target` is not absent, it is a few seconds away, and `docker load` against
|
|
// one blocks silently rather than failing (04-ISSUES/024). Waiting is the honest reading.
|
|
if err := waitForRuntime(ctx, d.Run, o.Timeout, o.Wait, say); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 4. Can this machine reach what the bundle's images come from?
|
|
//
|
|
// Asked of the hosts the bundle actually names rather than of the internet in general. The
|
|
// mesh's own image is carried and needs nothing served — it is skipped here for exactly that
|
|
// reason. Everything else is somebody else's image at somebody else's registry, and a machine
|
|
// that cannot reach it fails inside a pull, which reports a network error where a person
|
|
// reads a missing image.
|
|
//
|
|
// "The mesh's own image is skipped" used to mean "skipped if the template happened to name it
|
|
// in a way that needs no registry", and that is not the same sentence. A template names the
|
|
// control plane by SOMETHING — the reference is a slot, and step 3 replaces whatever is in it
|
|
// with the id of the image this installer carries. Whatever the slot held is therefore never
|
|
// pulled, never fetched, and never reached; requiring it to be reachable refuses a correct
|
|
// install because of a string that is about to be thrown away. Found on the first real run: the
|
|
// lab's template still carried `192.0.2.250:5000/mesh-control@…`, the address of a registry that
|
|
// no longer exists, and preflight timed out dialling it.
|
|
for _, host := range registriesIn(parsed) {
|
|
dialing, cancel := context.WithTimeout(ctx, o.Timeout)
|
|
err := d.Dial(dialing, host)
|
|
cancel()
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"this machine cannot reach %s, and the bundle's images are served from there: "+
|
|
"%w\nThe apply would fail inside a pull, which says the wrong thing. Fix the "+
|
|
"machine's network, or point the bundle at a registry it can reach",
|
|
host, err)
|
|
}
|
|
say(" reachable " + host)
|
|
}
|
|
return template, nil
|
|
}
|
|
|
|
// waitForRuntime asks the runtime, repeatedly, until it answers or the wait runs out.
|
|
func waitForRuntime(ctx context.Context, run Runner, probe, wait time.Duration, say func(string)) error {
|
|
detector := containerRuntimeDetector(run)
|
|
|
|
deadline := time.Now().Add(wait)
|
|
var last string
|
|
for {
|
|
probing, cancel := context.WithTimeout(ctx, probe)
|
|
verdict := detector.Detect(probing)
|
|
cancel()
|
|
if verdict.Present {
|
|
say(" container runtime " + verdict.Detail)
|
|
return nil
|
|
}
|
|
last = verdict.Detail
|
|
|
|
if time.Now().After(deadline) {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(runtimeAskEvery):
|
|
}
|
|
}
|
|
return fmt.Errorf(
|
|
"this machine has no container runtime that answers, after waiting %s: %s\n"+
|
|
"An installed package is not a capability (novox/hq 04-ISSUES/007) — the daemon was "+
|
|
"asked and did not reply. Start it, then run this again; every step is idempotent",
|
|
wait, last)
|
|
}
|
|
|
|
// runtimeAskEvery is how often the runtime is asked again while waiting for it.
|
|
var runtimeAskEvery = 2 * time.Second
|
|
|
|
// containerRuntimeDetector is the host's OWN detector for a working runtime, not a second
|
|
// implementation of the same question. Two answers to "is there a container runtime here" is how
|
|
// the installer and the host come to disagree about a machine.
|
|
func containerRuntimeDetector(run Runner) profile.Detector {
|
|
for _, detector := range profile.Default(profile.Runner(run)) {
|
|
if detector.Name() == profile.CapContainerRuntime {
|
|
return detector
|
|
}
|
|
}
|
|
// Unreachable unless the host's own detector set loses its container runtime, which would be
|
|
// a change nobody would make on purpose — said rather than nil-dereferenced.
|
|
panic("the host detects no container runtime capability, and the installer needs that answer")
|
|
}
|
|
|
|
// registriesIn is every host the bundle's images would be fetched from, without duplicates and in
|
|
// the order they appear.
|
|
//
|
|
// The control plane's own resource is excluded by identity rather than by the shape of what it
|
|
// names. Its image reference is a slot the installer overwrites with the id of the image it
|
|
// carries, so no registry ever serves it — and a template that filled that slot with a registry
|
|
// this machine cannot reach is not a machine with a network problem.
|
|
func registriesIn(d *declaration.Declaration) []string {
|
|
var hosts []string
|
|
seen := map[string]bool{}
|
|
for _, r := range d.Resources {
|
|
container, ok := r.(*declaration.Container)
|
|
if !ok || container.Identity() == ControlPlaneID {
|
|
continue
|
|
}
|
|
host, served := registryOf(container.Image)
|
|
if !served || seen[host] {
|
|
continue
|
|
}
|
|
seen[host] = true
|
|
hosts = append(hosts, host)
|
|
}
|
|
return hosts
|
|
}
|
|
|
|
// registryOf says where an image would be fetched from, and whether anything has to serve it.
|
|
//
|
|
// The second return is false for an image named by the digest of its own configuration: nothing
|
|
// serves those and nothing can (see `internal/declaration`'s checkImage). That is the whole reason
|
|
// the mesh's own control plane can be raised on a machine with no registry anywhere.
|
|
//
|
|
// The rule for the rest is the container runtime's own: the part before the first slash is a
|
|
// registry host if it looks like one — it has a dot, or a port, or it is `localhost` — and
|
|
// otherwise it is part of a repository name on the default registry.
|
|
func registryOf(reference string) (string, bool) {
|
|
if reference == "" || strings.HasPrefix(reference, "sha256:") {
|
|
return "", false
|
|
}
|
|
name := reference
|
|
if at := strings.Index(name, "@"); at >= 0 {
|
|
name = name[:at]
|
|
}
|
|
|
|
first, _, hasPath := strings.Cut(name, "/")
|
|
if !hasPath || !(strings.Contains(first, ".") || strings.Contains(first, ":") || first == "localhost") {
|
|
return DefaultRegistry, true
|
|
}
|
|
if !strings.Contains(first, ":") {
|
|
// A registry with no port is reached over HTTPS, which is where a pull would go.
|
|
return first + ":443", true
|
|
}
|
|
return first, true
|
|
}
|
|
|
|
func firstOr(values []string, fallback string) string {
|
|
if len(values) == 0 || strings.TrimSpace(values[0]) == "" {
|
|
return fallback
|
|
}
|
|
return values[0]
|
|
}
|