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.
189 lines
6.7 KiB
Go
189 lines
6.7 KiB
Go
// Command mesh-control is the control plane: everything that needs to know about more than one
|
|
// node (novox/hq ADR 0006).
|
|
//
|
|
// It runs as one process holding several contexts, each owning its own store. Today it holds one,
|
|
// `inventory`, and does one thing with it — brings its schema up to date, which is step 3 of the
|
|
// bootstrap in novox/hq 07-the-substrate and the step the first node cannot get past without.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/novox/mesh-control/internal/identity"
|
|
"github.com/novox/mesh-control/internal/inventory"
|
|
"github.com/novox/mesh-control/internal/licences"
|
|
"github.com/novox/mesh-control/internal/link"
|
|
"github.com/novox/mesh-control/internal/store"
|
|
)
|
|
|
|
// version is stamped at link time. Unset in a development build, and it says so rather than
|
|
// claiming a number.
|
|
var version = "development build"
|
|
|
|
// held is a context this process was granted, and the schema it carries.
|
|
//
|
|
// novox/hq ADR 0006 names seven. One is built. The list is short because the others do not exist
|
|
// yet, not because they are optional.
|
|
var held = []struct {
|
|
name string
|
|
migrations func() ([]store.Migration, error)
|
|
}{
|
|
{inventory.Name, inventory.Migrations},
|
|
{identity.Name, identity.Migrations},
|
|
{licences.Name, licences.Migrations},
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "mesh-control: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
args := os.Args[1:]
|
|
if len(args) == 0 {
|
|
usage()
|
|
return fmt.Errorf("no command given")
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
switch args[0] {
|
|
case "build":
|
|
return buildCommand(ctx, args[1:])
|
|
case "builder":
|
|
return builderCommand(ctx, args[1:])
|
|
case "board":
|
|
return boardCommand(ctx, args[1:])
|
|
case "licence":
|
|
return licenceCommand(ctx, args[1:])
|
|
case "rotate":
|
|
return rotateCommand(ctx, args[1:])
|
|
case "builds":
|
|
return buildsCommand(ctx, args[1:])
|
|
case "pin":
|
|
return pinCommand(ctx, args[1:], true)
|
|
case "unpin":
|
|
return pinCommand(ctx, args[1:], false)
|
|
case "migrate":
|
|
return migrate(ctx)
|
|
case "node":
|
|
return nodeCommand(ctx, args[1:])
|
|
case "token":
|
|
return tokenCommand(ctx, args[1:])
|
|
case "identity":
|
|
return identityCommand(ctx, args[1:])
|
|
case "broker":
|
|
return brokerCommand(args[1:])
|
|
case "serve":
|
|
return serve(ctx)
|
|
case "declare":
|
|
return declare(ctx, args[1:])
|
|
case "overlay":
|
|
return overlayCommand(ctx, args[1:])
|
|
case "module":
|
|
return moduleCommand(ctx, args[1:])
|
|
case "assign", "unassign":
|
|
return assignCommand(ctx, args[0], args[1:])
|
|
case "settings":
|
|
return settingsCommand(ctx, args[1:])
|
|
case "plan":
|
|
return planCommand(ctx, args[1:])
|
|
case "push":
|
|
return pushCommand(ctx, args[1:])
|
|
case "status":
|
|
return statusCommand(ctx, args[1:])
|
|
case "version":
|
|
fmt.Println(version)
|
|
return nil
|
|
case "help", "-h", "--help":
|
|
usage()
|
|
return nil
|
|
default:
|
|
usage()
|
|
return fmt.Errorf("%q is not a command", args[0])
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprint(os.Stderr, `mesh-control — the control plane
|
|
|
|
migrate bring each context's schema up to date
|
|
node add <name> create a node record
|
|
node list the nodes this mesh knows about
|
|
node show <name> what one machine reported it can do, and why
|
|
token issue --node <name> a one-time right to join, for an existing record
|
|
token issue --new <name> create the record and issue for it
|
|
identity show this control plane's signing key
|
|
broker show where the broker is, and what to expect there
|
|
serve consume what nodes say, and answer
|
|
declare <node> <file> send a node a signed declaration
|
|
overlay place <node> [flags] say where a node is and how it is reached
|
|
overlay show the private network, as the mesh computes it
|
|
module add <file> register a module from its manifest
|
|
module list what modules this mesh knows about
|
|
module moved <name> <commit> the source has a newer commit than the mesh built
|
|
module forget <name> remove one, unless a node is running it
|
|
status [--json] what is wrong, what is quiet, and what is out of date
|
|
board [--listen ADDR] the same three questions, as a page that holds nothing
|
|
assign <node> <module> put a module on a node
|
|
unassign <node> <module> take it off
|
|
settings set <module> <file> what a module's config should say, for the whole mesh
|
|
settings set <module> <file> --node <n> ...or for one machine
|
|
settings clear <module> [--node <n>] take a layer away
|
|
build <repository> [--ref R] have a build machine build it, and record what came out
|
|
build --behind build every module the mesh holds older than its source
|
|
builds [<module>] what has been built lately, and what came of it
|
|
builder issue <name> a broker account for a build machine, scoped to build work
|
|
licence add|list|use|key model access, under the name a person calls it
|
|
rotate <provision> [--consumer <n>] a new credential for every holder, both ends at once
|
|
pin <node> <provision> <from> which node this one gets a provision from
|
|
unpin <node> <provision> put that question back
|
|
plan <node> [--files|--json] what that node would run, and why
|
|
push [<node>] [--behind] send a node everything it should be, or only those that need it
|
|
version what this binary is
|
|
|
|
Each context reaches its own store through its own credential (novox/hq ADR 0008), named
|
|
`+store.Variable("<context>")+`. This process holds:
|
|
|
|
`)
|
|
for _, c := range held {
|
|
fmt.Fprintf(os.Stderr, " %-12s database %-12s from %s\n",
|
|
c.name, store.Database(c.name), store.Variable(c.name))
|
|
}
|
|
fmt.Fprintln(os.Stderr)
|
|
}
|
|
|
|
// parseAround reads flags that may sit before, after or between positional arguments.
|
|
//
|
|
// The standard library stops at the first non-flag argument, so `module add thing.json --source x`
|
|
// parses no flags at all and silently ignores every one of them. The host learned this the same
|
|
// way and says so in its own parser: a flag that is quietly dropped is the fault this project
|
|
// keeps naming, and it looks exactly like success.
|
|
func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
|
|
var positionals []string
|
|
rest := args
|
|
for {
|
|
if err := set.Parse(rest); err != nil {
|
|
return nil, err
|
|
}
|
|
rest = set.Args()
|
|
if len(rest) == 0 {
|
|
return positionals, nil
|
|
}
|
|
positionals = append(positionals, rest[0])
|
|
rest = rest[1:]
|
|
}
|
|
}
|
|
|
|
func (b builds) Built(ctx context.Context, result link.BuildResult) error {
|
|
return b.inv.RecordBuild(ctx, buildFrom(result))
|
|
}
|