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 , module list, or module forget ") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory 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 [--source --ref --commit ]") } 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 — 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 ") } 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 ", verb) } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory 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, open, 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 [--node ], or settings clear [--node ]") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory 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 [--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 [--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 ") } if !setting && len(args) != 2 { return errors.New("unpin ") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory 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 }