Both branches changed the seat set from the same starting point, so every number collided and every `mesh-*` name existed twice. The trunk's numbers and names win: this branch's records became 0129/0130 and its migrations 0037/0038, and the hardcoded rename map gave way to the trunk's `seat_alias` table — a rename is a row now (ADR 0122), not a recompile. Three of my checks were wrong and the merge is what showed it: A seat with an empty protocol is a marker, not an incomplete declaration. Most node-scoped seats are markers — which module is this machine's packet filter — and refusing one refused most of the set, the showcase module included. A mistyped field name is already refused by the parser, so an empty protocol was written as one deliberately. A claim on a seat this manifest does not declare is not the parser's to judge. A module may hold a seat another module declared; that is the whole reason ADR 0126 has callers name the seat and not its provider. Whether the seat exists is a fact about the catalogue, so the refusal is at registration, where every declaration is in view. And a seat may share a name with the provision it delivers. `git`, the npm registry and the artifact store still do, because renaming a delivering seat cascades to every consumer requiring it, with a window where a holder stops resolving mid-flight. The trunk deferred exactly those three on purpose. Full suite green against a real NATS and store.
238 lines
9.8 KiB
Go
238 lines
9.8 KiB
Go
// Command mesh-controller 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-foundation and the step the first node cannot get past without.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/novox/mesh-controller/internal/identity"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/licences"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/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-controller: %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 "api":
|
|
return apiCommand(ctx, args[1:])
|
|
case "licence":
|
|
return licenceCommand(ctx, args[1:])
|
|
case "rotate":
|
|
return rotateCommand(ctx, args[1:])
|
|
case "ask":
|
|
return askCommand(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 "upgrade":
|
|
return upgradeCommand(ctx, args[1:])
|
|
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 "take":
|
|
return takeCommand(ctx, args[1:])
|
|
case "converge":
|
|
return convergeCommand(ctx, args[1:])
|
|
case "adopt":
|
|
return adoptCommand(ctx, args[1:])
|
|
case "settings":
|
|
return settingsCommand(ctx, args[1:])
|
|
case "secret":
|
|
return secretCommand(ctx, args[1:])
|
|
case "operator":
|
|
return operatorCommand(ctx, args[1:])
|
|
case "plan":
|
|
return planCommand(ctx, args[1:])
|
|
case "push":
|
|
return pushCommand(ctx, args[1:])
|
|
case "rollout":
|
|
return rolloutCommand(ctx, args[1:])
|
|
case "seats":
|
|
return seatsCommand(ctx, args[1:])
|
|
case "seat":
|
|
return seatCommand(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-controller — the control plane
|
|
|
|
migrate bring each context's schema up to date
|
|
node add <name> [--adopted] create a node record; --adopted: the machine is in use
|
|
node list the nodes this mesh knows about
|
|
node show <name> what one machine reported it can do, and why
|
|
node public-domain <name> the domain it composes its routed names under
|
|
node public-domain <name> <d> ...set it to d
|
|
node public-domain <name> --clear ...it faces the outside no longer
|
|
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
|
|
token issue ... --adopted ...for a machine in use, which joins adopted
|
|
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 runs it or the mesh holds things for it
|
|
module forget <name> --and-what-it-holds ...and discard its settings, secrets and ports too
|
|
module issue <name> --node <m> a broker account for a module, scoped to its emits and consumes
|
|
upgrade <name> what happens when this module's current version moves
|
|
upgrade <name> roll-out [--together] ...send it to the machines running it
|
|
upgrade <name> record ...record that they are behind, and send nothing
|
|
status [--json] what is wrong, what is quiet, and what is out of date
|
|
seats [--json] every seat this mesh defines, what it delivers, and who holds it
|
|
board [--listen ADDR] the same three questions, as a page that holds nothing
|
|
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
|
|
assign <node> <module> put a module on a node
|
|
unassign <node> <module> take it off
|
|
take <node> <module> cut a module over on an adopted node, once its data has moved
|
|
converge <node> [--yes <digest>] [--filter nftables] preview, then make, an adopted node converged
|
|
adopt <node> return a converged node to adopted; what was taken stays taken
|
|
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
|
|
secret accept <node> <module> <name> carry a value the mesh did not make and cannot invent
|
|
secret accept ... --from <file> ...read it from a file rather than being asked
|
|
secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>]
|
|
break-glass: open the operator-sealed copy, to a 0600 file
|
|
secret export [--out <file>] every operator-sealed copy, ciphertext — keep it with the key
|
|
operator key make [--out <file>] make the operator's sealing key, off the mesh; private half to the file only
|
|
operator key set <public> [--replace] tell the mesh which operator key to seal to
|
|
operator key show the operator key, and what it can recover
|
|
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,
|
|
delivered as the builder module's broker secret (module add it first)
|
|
licence add|list|use|key model access, under the name a person calls it
|
|
licence manager <name> <node> the node that holds a refreshable licence's refresh token
|
|
licence refresh <name> mint a new access token and seal it to every holder
|
|
rotate <provision> [--consumer <n>] a new credential for every holder, both ends at once
|
|
ask <module> <tool> [json] call one of a module's tools over the broker, and print its answer
|
|
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>")+` — or `+store.FileVariable("<context>")+`, naming a file that holds
|
|
the same thing and keeps the password out of the environment. 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))
|
|
}
|