2,769 lines and 59 functions, holding command parsing, store opening, resolution, the board, rotation, licences, builds and status rendering. Nothing in it was wrong. It grew because appending was always the cheapest next step, and no single edit was the one that should have been a new file. That is exactly how novox/hq ADR 0001 records `hal/sdk` reaching 155 files and 34,636 lines — "containing code from every context", with each addition avoiding a cycle and none of them the mistake. This is the same shape at 8% of the size, which is why it is worth doing now rather than noting. Eight files, along boundaries that already existed: what a machine is; the private network; the catalogue; working out what one machine should be; sending it; builds; the three questions; and reaching each context's store. main.go keeps what a main is for — parsing arguments and dispatching. A pure move. No behaviour changed, no test changed, and the gate is green before and after — which is the only thing that makes a refactor this size safe to do in one commit.
351 lines
10 KiB
Go
351 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-control/internal/catalogue"
|
|
"github.com/novox/mesh-control/internal/inventory"
|
|
"github.com/novox/mesh-control/internal/overlay"
|
|
)
|
|
|
|
// the catalogue: what exists, what is assigned, and how it is configured.
|
|
//
|
|
// Split out of main.go, which had reached 2,769 lines because appending was always the
|
|
// cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636:
|
|
// nothing in it was wrong, and no one edit was the one that should have been a new file.
|
|
|
|
// provided is what comes with the control plane rather than from a repository.
|
|
//
|
|
// WireGuard, the names, and the domain module over both. The first two are here because the code
|
|
// that works out their files is here:
|
|
// a peer list is derived from every machine at once, so it cannot be written in a manifest, and
|
|
// whatever computes it has to live wherever the whole picture is.
|
|
//
|
|
// **It is a module in every other respect** — assigned, unassigned, resolved, settled, and absent
|
|
// from a machine nobody gave it to.
|
|
func providedModules() []catalogue.Manifest {
|
|
var out []catalogue.Manifest
|
|
for _, raw := range []map[string]any{
|
|
overlay.Manifest(), overlay.NamesManifest(), overlay.ResolverManifest(),
|
|
overlay.DomainManifest(),
|
|
} {
|
|
var m catalogue.Manifest
|
|
b, _ := json.Marshal(raw)
|
|
_ = json.Unmarshal(b, &m)
|
|
out = append(out, m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
var provided = providedModules()
|
|
|
|
func moduleCommand(ctx context.Context, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("module add <file>, module list, or module forget <name>")
|
|
}
|
|
inv, err := openInventory(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer inv.Close()
|
|
|
|
switch args[0] {
|
|
case "add":
|
|
set := flag.NewFlagSet("module add", flag.ContinueOnError)
|
|
repo := set.String("source", "", "where this module comes from")
|
|
ref := set.String("ref", "", "the branch followed there")
|
|
commit := set.String("commit", "", "the commit this manifest was read at")
|
|
positionals, err := parseAround(set, args[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("module add <manifest.json> [--source <repo> --ref <branch> --commit <sha>]")
|
|
}
|
|
raw, err := os.ReadFile(positionals[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m, err := catalogue.ParseManifest(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Provenance together or not at all. A source with no commit cannot be compared against
|
|
// anything, so it would record where the module came from and still never be able to say
|
|
// the mesh is behind it — which is the one thing recording it is for.
|
|
if (*repo == "") != (*commit == "") {
|
|
return errors.New("--source and --commit go together: a source with no commit " +
|
|
"cannot be compared against anything, and a commit with no source has nothing " +
|
|
"to be compared with")
|
|
}
|
|
if err := inv.RegisterModule(ctx, m, inventory.Source{
|
|
Repository: *repo, Ref: *ref, BuiltFrom: *commit,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s registered", m.Module)
|
|
if *commit != "" {
|
|
fmt.Printf(" from %s", short(*commit))
|
|
}
|
|
if len(m.Provides) > 0 {
|
|
fmt.Printf(", providing %s", describeOffers(m.Provides))
|
|
}
|
|
fmt.Println()
|
|
for _, c := range m.Claims {
|
|
fmt.Printf(" claims %s, one per %s\n", c.Name, c.At())
|
|
}
|
|
return nil
|
|
|
|
case "list":
|
|
// The catalogue: what exists, where it came from, whether it is current, and who runs it.
|
|
// The provenance was recorded from the first build and nothing showed it, which made
|
|
// "is this current?" a question you could only answer by reading the database.
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(entries) == 0 {
|
|
fmt.Println("this mesh knows about no modules yet")
|
|
return nil
|
|
}
|
|
var stale int
|
|
for _, e := range entries {
|
|
m := e.Manifest
|
|
fmt.Printf("%-18s %-8s", m.Module, m.Version)
|
|
|
|
switch {
|
|
case e.Provided:
|
|
fmt.Printf(" %-22s", "with the control plane")
|
|
case e.Source.Repository == "":
|
|
// Handed over by hand. Legitimate — it is how a module is fixed in a hurry — and
|
|
// worth saying, because nothing can rebuild it.
|
|
fmt.Printf(" %-22s", "handed over")
|
|
case !e.Source.Current():
|
|
stale++
|
|
fmt.Printf(" %-22s", "behind "+short(e.Source.BuiltFrom)+" < "+short(e.Source.Head))
|
|
default:
|
|
fmt.Printf(" %-22s", "built "+short(e.Source.BuiltFrom))
|
|
}
|
|
|
|
if len(e.On) > 0 {
|
|
fmt.Printf(" on %s", strings.Join(e.On, ", "))
|
|
} else {
|
|
fmt.Printf(" on nothing")
|
|
}
|
|
fmt.Println()
|
|
|
|
var says []string
|
|
if len(m.Provides) > 0 {
|
|
says = append(says, "provides "+describeOffers(m.Provides))
|
|
}
|
|
if len(m.Requires) > 0 {
|
|
says = append(says, "requires "+strings.Join(m.Requires, ", "))
|
|
}
|
|
for _, c := range m.Claims {
|
|
says = append(says, "claims "+c.At()+"/"+c.Name)
|
|
}
|
|
if len(m.Capabilities) > 0 {
|
|
says = append(says, "needs "+strings.Join(m.Capabilities, ", "))
|
|
}
|
|
if len(says) > 0 {
|
|
fmt.Printf(" %s\n", strings.Join(says, " · "))
|
|
}
|
|
}
|
|
if stale > 0 {
|
|
fmt.Printf("\n%d module(s) behind their source — `build --behind` to catch up\n", stale)
|
|
}
|
|
return nil
|
|
|
|
case "moved":
|
|
if len(args) != 3 {
|
|
return errors.New("module moved <name> <commit> — the source has a newer commit")
|
|
}
|
|
if err := inv.SourceMoved(ctx, args[1], args[2]); err != nil {
|
|
return err
|
|
}
|
|
from, err := inv.SourceOf(ctx, args[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if from.Current() {
|
|
fmt.Printf("%s is current at %s\n", args[1], short(from.Head))
|
|
return nil
|
|
}
|
|
fmt.Printf("%s is behind: the mesh holds %s and the source has %s\n",
|
|
args[1], short(from.BuiltFrom), short(from.Head))
|
|
fmt.Printf(" run `build %s` to catch up\n", from.Repository)
|
|
return nil
|
|
|
|
case "forget":
|
|
if len(args) != 2 {
|
|
return errors.New("module forget <name>")
|
|
}
|
|
if err := inv.ForgetModule(ctx, args[1]); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s forgotten\n", args[1])
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("module has no %q; it has add, list, moved and forget", args[0])
|
|
}
|
|
}
|
|
|
|
func assignCommand(ctx context.Context, verb string, args []string) error {
|
|
if len(args) != 2 {
|
|
return fmt.Errorf("%s <node> <module>", verb)
|
|
}
|
|
inv, err := openInventory(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer inv.Close()
|
|
|
|
if verb == "unassign" {
|
|
if err := inv.Unassign(ctx, args[0], args[1]); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s no longer runs %s — run `push %s` to make it so\n", args[0], args[1], args[0])
|
|
return nil
|
|
}
|
|
if err := inv.Assign(ctx, args[0], args[1]); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s is assigned %s\n", args[0], args[1])
|
|
|
|
// Resolved immediately, because an assignment that cannot be applied should be said now
|
|
// rather than at the next push. The assignment is kept either way: it is what a person meant,
|
|
// and the refusal is about the set rather than about this one.
|
|
if _, _, err := planFor(ctx, inv, args[0]); err != nil {
|
|
fmt.Println()
|
|
return err
|
|
}
|
|
fmt.Printf(" run `push %s` to send it\n", args[0])
|
|
return nil
|
|
}
|
|
|
|
func settingsCommand(ctx context.Context, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("settings set <module> <file> [--node <node>], or settings clear <module> [--node <node>]")
|
|
}
|
|
inv, err := openInventory(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer inv.Close()
|
|
|
|
set := flag.NewFlagSet("settings", flag.ContinueOnError)
|
|
node := set.String("node", "", "one machine, rather than the whole mesh")
|
|
positionals, err := parseAround(set, args[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
where := "the whole mesh"
|
|
if *node != "" {
|
|
where = *node
|
|
}
|
|
|
|
switch args[0] {
|
|
case "set":
|
|
if len(positionals) != 2 {
|
|
return errors.New("settings set <module> <settings.json> [--node <node>]")
|
|
}
|
|
raw, err := os.ReadFile(positionals[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var values map[string]any
|
|
if err := json.Unmarshal(raw, &values); err != nil {
|
|
return fmt.Errorf("%s is not a settings file: %w", positionals[1], err)
|
|
}
|
|
if err := inv.SetSettings(ctx, *node, positionals[0], values); err != nil {
|
|
return err
|
|
}
|
|
|
|
var keys []string
|
|
for k := range values {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
fmt.Printf("%s on %s: %s\n", positionals[0], where, strings.Join(keys, ", "))
|
|
fmt.Println(" run `push` to send it")
|
|
return nil
|
|
|
|
case "clear":
|
|
if len(positionals) != 1 {
|
|
return errors.New("settings clear <module> [--node <node>]")
|
|
}
|
|
if err := inv.ClearSettings(ctx, *node, positionals[0]); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s on %s is back to what the module says\n", positionals[0], where)
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("settings has no %q; it has set and clear", args[0])
|
|
}
|
|
}
|
|
|
|
// describeOffers says what a module provides, and marks the ones answered from anywhere in the
|
|
// mesh — because "provides a database" and "provides a shell" are read the same way and mean
|
|
// entirely different things about where the answer has to be.
|
|
func describeOffers(offers []catalogue.Offer) string {
|
|
var out []string
|
|
for _, o := range offers {
|
|
if o.At() == catalogue.ScopeMesh {
|
|
out = append(out, o.Name+" (from anywhere in the mesh)")
|
|
continue
|
|
}
|
|
out = append(out, o.Name)
|
|
}
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
// pinCommand says which node a machine gets a provision from.
|
|
//
|
|
// Needed only when more than one could answer, and recordable before that -- a mesh with one
|
|
// database should not change where an existing machine gets its data the day a second one
|
|
// arrives.
|
|
func pinCommand(ctx context.Context, args []string, setting bool) error {
|
|
if setting && len(args) != 3 {
|
|
return errors.New("pin <node> <provision> <from-node>")
|
|
}
|
|
if !setting && len(args) != 2 {
|
|
return errors.New("unpin <node> <provision>")
|
|
}
|
|
inv, err := openInventory(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer inv.Close()
|
|
|
|
if !setting {
|
|
if err := inv.UnpinProvision(ctx, args[0], args[1]); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s is no longer told where to get %s from\n", args[0], args[1])
|
|
return nil
|
|
}
|
|
if args[0] == args[2] {
|
|
// Allowed by nothing here, and worth saying rather than resolving into a confusing
|
|
// refusal later: a node providing something to itself is a node-scoped provision, and
|
|
// this field is for the other kind.
|
|
return fmt.Errorf("%s cannot get %s from itself; that would be a provision this machine "+
|
|
"provides, which does not need saying", args[0], args[1])
|
|
}
|
|
if err := inv.PinProvision(ctx, args[0], args[1], args[2]); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s gets %s from %s\n", args[0], args[1], args[2])
|
|
fmt.Printf(" run `push %s` to send it\n", args[0])
|
|
return nil
|
|
}
|