223 lines
8.0 KiB
Go
223 lines
8.0 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Phase two — a mesh that runs becomes a mesh that works.
|
|
//
|
|
// Genesis ends with a control plane, a store, a broker, a registry and a builder — a mesh that
|
|
// RUNS. It holds no module graph, has no private network, and filters nothing. Those used to be
|
|
// things somebody typed afterwards, which is how they went missing for weeks without anything
|
|
// complaining (novox/hq 03-DESIGN/01-to-be/21-the-installation-in-full.md). The installer goes as
|
|
// far as it can instead, and asks where a human must choose.
|
|
//
|
|
// Everything here is `module add`, `build`, `assign` and `push` — the same verbs a person types,
|
|
// through the same commands, so what the installer does and what an operator does remain one act.
|
|
|
|
// buildWait bounds one module build. Generous, because the first build compiles a toolchain.
|
|
const buildWait = 20 * time.Minute
|
|
|
|
// BuildBase asks the mesh to build the shared base every module with code of its own stands on.
|
|
//
|
|
// **First, because until it exists nothing else with code can be built.** Not registered as a
|
|
// module here: it is never assigned — it runs nowhere — and the build itself records what was
|
|
// made, which is all anything downstream reads.
|
|
func BuildBase(ctx context.Context, o Options, control controlPlane, say func(string)) error {
|
|
if o.ToolsSource.Repository == "" {
|
|
return fmt.Errorf("phase two needs --tools-source: the shared base is built from its " +
|
|
"own repository, and an installer told nothing cannot know where that is")
|
|
}
|
|
say(" building " + o.ToolsSource.Repository + " at " + refOr(o.ToolsSource.Ref))
|
|
_, err := control.within(buildWait).tell(ctx,
|
|
"build", o.ToolsSource.Repository, "--ref", refOr(o.ToolsSource.Ref), "--wait", "1200s")
|
|
return err
|
|
}
|
|
|
|
// InstallFromCatalogue builds a catalogue module and installs it on this machine.
|
|
//
|
|
// The order matters and is the one the lab proved: register the manifest, build (so the artifact
|
|
// exists before anything resolves it), issue its broker account (a runtime without one starts,
|
|
// parses a password as a credential document, and loops), assign, push.
|
|
func InstallFromCatalogue(ctx context.Context, o Options, control controlPlane,
|
|
module string, say func(string)) error {
|
|
|
|
manifest, err := readManifest(o.Catalogue, module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remote := "/" + module + "-module.json"
|
|
if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil {
|
|
return err
|
|
}
|
|
if _, err := control.tell(ctx, "module", "add", remote); err != nil {
|
|
return err
|
|
}
|
|
say(" registered " + module)
|
|
|
|
if builds(manifest) {
|
|
if o.CatalogSource.Repository == "" {
|
|
return fmt.Errorf("%s has to be built and there is no --catalog-source to build it "+
|
|
"from: the catalogue CHECKOUT says what it is, the catalogue REPOSITORY is where "+
|
|
"a builder clones it", module)
|
|
}
|
|
say(" building " + module)
|
|
if _, err := control.within(buildWait).tell(ctx, "build", o.CatalogSource.Repository,
|
|
"--path", "modules/"+module, "--ref", refOr(o.CatalogSource.Ref),
|
|
"--wait", "1200s"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := control.tell(ctx, "module", "issue", module, "--node", o.Node); err != nil {
|
|
// Not every module consumes the broker; one that does not is refused an account and that
|
|
// is fine. Said rather than silent, so a module that SHOULD have one and was refused is
|
|
// visible here rather than as a crash-loop later.
|
|
say(" no account " + module + " — it declares nothing to say on the broker")
|
|
} else {
|
|
say(" account issued " + module)
|
|
}
|
|
|
|
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
|
|
return err
|
|
}
|
|
if err := prepareModule(ctx, o, control, module, say); err != nil {
|
|
return err
|
|
}
|
|
if _, err := pushNode(ctx, o, control, say); err != nil {
|
|
return err
|
|
}
|
|
say(" installed " + module)
|
|
return nil
|
|
}
|
|
|
|
// PlaceOnTheNetwork chooses a private-network provider, assigns it, and places this machine as
|
|
// the hub.
|
|
//
|
|
// **Assigning is not being on the network** — a lesson paid for: the module installed, the names
|
|
// file was written with no names in it, and everything reported success, because nobody had said
|
|
// where this machine IS. So placement is part of the step, not a separate act.
|
|
func PlaceOnTheNetwork(ctx context.Context, o Options, control controlPlane,
|
|
brokerAddress string, say func(string)) error {
|
|
|
|
network, err := decide(Choice{
|
|
Name: "private-network",
|
|
Question: "Which private network should this mesh run?",
|
|
Options: []string{"wireguard"},
|
|
}, o.Answers["private-network"], o.Prompt, say)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Today the one provider is the control plane's own computed module. The choice exists so
|
|
// that the day there are two, this asks instead of assuming.
|
|
module := "networking"
|
|
_ = network
|
|
|
|
endpoint, err := decide(Choice{
|
|
Name: "endpoint",
|
|
Question: "Where do other machines reach this one for the private network? " +
|
|
"(host:port; the host other machines dial)",
|
|
Default: derivedEndpoint(brokerAddress, o.Ports.orDefaults().Hub),
|
|
}, o.Answers["endpoint"], o.Prompt, say)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if endpoint == "" {
|
|
return fmt.Errorf("the private network needs an endpoint other machines can dial, and " +
|
|
"nothing said one: pass --endpoint, or --broker-address so one can be derived")
|
|
}
|
|
|
|
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
|
|
return err
|
|
}
|
|
if _, err := control.tell(ctx, "overlay", "place", o.Node,
|
|
"--hub", "--endpoint", endpoint, "--site", o.Site); err != nil {
|
|
return err
|
|
}
|
|
if _, err := pushNode(ctx, o, control, say); err != nil {
|
|
return err
|
|
}
|
|
say(" on the network " + o.Node + " is the hub, at " + endpoint)
|
|
return nil
|
|
}
|
|
|
|
// ChooseAndInstallFilter picks the packet filter — required, so the question is which, not
|
|
// whether — and installs it.
|
|
func ChooseAndInstallFilter(ctx context.Context, o Options, control controlPlane, say func(string)) error {
|
|
filter, err := decide(Choice{
|
|
Name: "packet-filter",
|
|
Question: "Which packet filter should this machine run?",
|
|
Options: []string{"nftables"},
|
|
}, o.Answers["packet-filter"], o.Prompt, say)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return InstallFromCatalogue(ctx, o, control, filter, say)
|
|
}
|
|
|
|
// InstallExtras installs what was asked for beyond the floor.
|
|
//
|
|
// One refusal per act: an extra that cannot be installed fails the run, because somebody asked
|
|
// for it by name and a mesh that reports success minus one thing is reporting the wrong thing.
|
|
func InstallExtras(ctx context.Context, o Options, control controlPlane, say func(string)) error {
|
|
asked, err := decide(Choice{
|
|
Name: "extras",
|
|
Question: "Anything beyond the floor? (comma-separated catalogue modules — " +
|
|
"gitea, step-ca, dnsmasq — or nothing)",
|
|
Default: "none",
|
|
}, strings.Join(o.Extras, ","), o.Prompt, say)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if asked == "" || asked == "none" {
|
|
say(" extras none")
|
|
return nil
|
|
}
|
|
for _, extra := range strings.Split(asked, ",") {
|
|
if extra = strings.TrimSpace(extra); extra == "" {
|
|
continue
|
|
}
|
|
if err := InstallFromCatalogue(ctx, o, control, extra, say); err != nil {
|
|
return fmt.Errorf("%s was asked for and could not be installed: %w", extra, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// builds says whether a manifest declares anything to build.
|
|
func builds(manifest []byte) bool {
|
|
var m struct {
|
|
Build *struct {
|
|
Artifacts []json.RawMessage `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
if err := json.Unmarshal(manifest, &m); err != nil {
|
|
return false
|
|
}
|
|
return m.Build != nil && len(m.Build.Artifacts) > 0
|
|
}
|
|
|
|
// derivedEndpoint is the default place other machines dial for the private network: the same host
|
|
// they already dial for the broker, on WireGuard's ordinary port. One fact, not two.
|
|
func derivedEndpoint(brokerAddress string, hub int) string {
|
|
host, _, err := net.SplitHostPort(brokerAddress)
|
|
if err != nil || host == "" {
|
|
return ""
|
|
}
|
|
return net.JoinHostPort(host, strconv.Itoa(hub))
|
|
}
|
|
|
|
func refOr(ref string) string {
|
|
if ref == "" {
|
|
return "main"
|
|
}
|
|
return ref
|
|
}
|