package bootstrap import ( "context" "encoding/json" "fmt" "net" "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 := 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.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) string { host, _, err := net.SplitHostPort(brokerAddress) if err != nil || host == "" { return "" } return net.JoinHostPort(host, "51820") } func refOr(ref string) string { if ref == "" { return "main" } return ref }