// Command mesh-bootstrap brings a mesh into existence on a bare machine. // // Tier 0, beside `mesh-host` and not inside it. Bootstrapping is done by hand and it changes a // machine, which is what tier 0 is (novox/hq 03-DESIGN/01-to-be/05-the-node-host.md) — but // `mesh-host` states of itself that it connects to nothing and listens on nothing and that what it // applies comes from a file, and that is the whole reason an always-running root daemon can be // audited by reading one page. An installer that loads images and interrogates a control plane // cannot be folded into it without making that sentence false. Same tier, same repository, // different program. // // Genesis is a pivot (novox/hq ADR 0067). It raises a substrate whose control plane is named by the // digest of its own configuration — legal exactly where nothing could have served an image — then // enrols this machine, installs the registry module, pushes that image into it to get the manifest // digest it has never had, reinstalls the control plane as an ordinary module pinned to it, and // drops the temporary one. Without --catalog it stops after the substrate and says why. package main import ( "context" "encoding/json" "flag" "fmt" "io" "net" "net/http" "os" "os/signal" "syscall" "time" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/bootstrap" "github.com/novox/mesh-host/internal/store" ) // version is stamped at build time. Unset in a development build, and said so rather than // defaulted to something that looks like a release. var version = "development build" const ( defaultTemplate = "substrate.lock" defaultOut = "/var/lib/mesh-host/substrate.lock" defaultRegistry = "127.0.0.1:5000" defaultHost = "/usr/local/bin/mesh-host" defaultService = "mesh-host.service" ) const usage = `mesh-bootstrap — make a bare machine into a mesh bootstrap the ten steps below (the default) version 1 preflight what has to be true before anything is changed 2 load the control plane's image, carried in this installer 3 bundle the substrate, named for this machine 4 apply raise it 5 verify it is up, and the control plane replies 6 enrol this machine becomes the mesh's first node 7 registry install the module that gives this mesh an image store 8 publish push the control plane's image into it, for its first digest 9 control reinstall the control plane as an ordinary module, pinned to that digest 10 retire drop the temporary control plane; the host removes it --bundle the substrate template to build this machine's bundle from (default ` + defaultTemplate + `) --out where the produced bundle is written, for a person to read (default ` + defaultOut + `) --state where this node records what it has applied (default ` + store.DefaultPath + `) --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 --node the name this machine is known by (default: its hostname) --registry where this mesh keeps its own images (default ` + defaultRegistry + `) every node pulls the control plane from this, so on a mesh of more than one machine it must be an address the others can reach --host the mesh-host binary on this machine (default ` + defaultHost + `) --host-service the unit that supervises it (default ` + defaultService + `) --host-in-background start the host unsupervised instead. It does not survive a reboot. This is what a lab does and what no real machine should --system which operating system this is; by default it is asked --timeout how long any single probe may take (default 30s) --wait how long a thing that is merely starting is given (default 3m) --dry-run everything that does not change the machine --json machine-readable output 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 called mesh-control, so they are two containers with two owners and there is nothing to hand over. Every step is idempotent: run it again after fixing whatever it named, and the steps that already succeeded say so. ` func main() { // Ctrl-C must stop the installer, not be swallowed by whatever it is waiting for — and it // waits on pulls, on a runtime starting, and on a control plane opening its stores. ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() command, opts, jsonOut, err := parseArgs(os.Args[1:]) if err == nil { err = run(ctx, command, opts, jsonOut) } if err != nil { fmt.Fprintf(os.Stderr, "mesh-bootstrap: %v\n", err) os.Exit(1) } } // parseArgs takes an optional subcommand first, then its flags. // // Parsed in a loop for the reason `mesh-host` records: the standard library stops at the FIRST // non-flag argument, so a flag sitting after one is silently dropped and the command exits zero // having ignored what it was asked. That fault has been paid for twice in this repository and is // not being paid for a third time. func parseArgs(args []string) (string, bootstrap.Options, bool, error) { opts := bootstrap.Options{ Template: defaultTemplate, Out: defaultOut, State: store.DefaultPath, Registry: defaultRegistry, Host: defaultHost, // The machine's own name, because that is what a person already calls it and an installer // inventing a different one would leave the mesh naming a machine nobody recognises. It is // read here rather than inside the bootstrap so that --node overrides a fact rather than a // default computed halfway through. Node: hostname(), HostService: defaultService, // Longer than the host's 10s: these probes reach a container runtime that may be busy // pulling, and a probe that times out on a working machine is a false refusal. Timeout: 30 * time.Second, // A socket-activated runtime queued behind the network, and a control plane running its // first `initdb`-shaped wait, are both minutes rather than seconds. Wait: 3 * time.Minute, } var jsonOut bool command := "bootstrap" if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' { command = args[0] args = args[1:] } set := newFlagSet(&opts, &jsonOut) var positionals []string rest := args for { if err := set.Parse(rest); err != nil { return "", opts, false, err } rest = set.Args() if len(rest) == 0 { break } positionals = append(positionals, rest[0]) rest = rest[1:] } // Refused rather than ignored: a mistyped argument that changes nothing and reports success is // worse than an error, and this program's whole job is to change a machine. if len(positionals) > 0 { return "", opts, false, fmt.Errorf( "unexpected argument %q — try `mesh-bootstrap help`", positionals[0]) } return command, opts, jsonOut, nil } func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet { set := flag.NewFlagSet("mesh-bootstrap", flag.ContinueOnError) set.SetOutput(os.Stderr) set.Usage = func() { fmt.Fprint(os.Stderr, usage) } set.StringVar(&opts.Template, "bundle", opts.Template, "the substrate template to build from") set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written") set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied") set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue, "a checkout of the mesh's catalogue; without it this stops after the substrate") 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.Host, "host", opts.Host, "the mesh-host binary on this machine") set.StringVar(&opts.HostService, "host-service", opts.HostService, "the unit that supervises it") set.BoolVar(&opts.HostInBackground, "host-in-background", false, "start the host unsupervised; it does not survive a reboot") set.StringVar(&opts.System, "system", opts.System, "which operating system this is") set.DurationVar(&opts.Timeout, "timeout", opts.Timeout, "how long any single probe may take") set.DurationVar(&opts.Wait, "wait", opts.Wait, "how long something merely starting is given") set.BoolVar(&opts.DryRun, "dry-run", false, "everything that does not change the machine") set.BoolVar(jsonOut, "json", false, "machine-readable output") return set } func run(ctx context.Context, command string, opts bootstrap.Options, jsonOut bool) error { switch command { case "bootstrap": say := func(line string) { if !jsonOut { fmt.Println(line) } } result, err := bootstrap.Run(ctx, opts, bootstrap.Deps{ Run: apply.ExecRunner, Dial: dial, Fetch: fetch, }, say) // Printed whichever way it went. What the installer got through before it stopped is on // the machine either way, and a report that only exists on success describes a machine // nobody has (novox/hq ADR 0018). if jsonOut { encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") if encodeErr := encoder.Encode(result); encodeErr != nil && err == nil { return encodeErr } } return err case "version": fmt.Println(version) return nil case "help", "-h", "--help": fmt.Fprint(os.Stderr, usage) return nil default: return fmt.Errorf("unknown command %q — try `mesh-bootstrap help`", command) } } // hostname is what this machine calls itself, or empty. // // Empty rather than a guess: a machine that cannot say its own name is one the installer must be // told about, and `mesh-bootstrap-0` would be a name in the mesh's records that matches nothing // anybody types anywhere else. The refusal happens at step 6, where the name is first needed. func hostname() string { name, err := os.Hostname() if err != nil { return "" } return name } // fetch asks an HTTP endpoint and reports what it said. // // Plain HTTP, and only at the mesh's own registry: it is reached over the mesh's private network, // which is already the encrypted and authenticated thing, and a second layer inside it would be // certificates to issue and rotate for no property the first does not have (mesh-control's // `internal/builder` pushes to it on the same reasoning). // // The body is read with a limit. What is asked for is a status and a short JSON answer, and a // registry that answered with a gigabyte would otherwise be an installer that never returns. func fetch(ctx context.Context, url string) (int, string, error) { request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return 0, "", err } response, err := http.DefaultClient.Do(request) if err != nil { return 0, "", err } defer response.Body.Close() said, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) if err != nil { return response.StatusCode, "", err } return response.StatusCode, string(said), nil } // dial answers whether a TCP address responds. // // A connection rather than a ping or a name lookup: what has to work is a pull, and a pull opens a // connection to exactly this address. A machine whose DNS resolves and whose route is missing // passes a lookup and fails the thing that matters. func dial(ctx context.Context, address string) error { var dialer net.Dialer conn, err := dialer.DialContext(ctx, "tcp", address) if err != nil { return err } return conn.Close() }