// 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" "errors" "flag" "fmt" "os" "os/signal" "syscall" "time" "github.com/novox/mesh-control/internal/inventory" "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}, } 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 "migrate": return migrate(ctx) case "node": return nodeCommand(ctx, args[1:]) case "token": return tokenCommand(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 create a node record node list the nodes this mesh knows about token issue --node a one-time right to join, for an existing record token issue --new create the record and issue for it version what this binary is Each context reaches its own store through its own credential (novox/hq ADR 0008), named `+store.Variable("")+`. 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) } // migrate brings every held context's schema up to date. // // Reported per context and per migration, because this runs during a bootstrap on a machine with // nothing else on it — the output is the only account of what happened, and "migrated" is not one. func migrate(ctx context.Context) error { for _, c := range held { migrations, err := c.migrations() if err != nil { return err } s, err := store.Open(ctx, c.name) if err != nil { return err } defer s.Close() // The bootstrap raises PostgreSQL moments before this runs, and a container that is // running is not a database that will answer — a distinction this project has already // paid for once, when a crash-looping database reported itself as up between restarts. if err := s.Ready(ctx, 60*time.Second); err != nil { return err } done, err := s.Migrate(ctx, migrations) for _, m := range done { fmt.Printf("%s: applied %04d-%s\n", c.name, m.Number, m.Name) } if err != nil { return err } if len(done) == 0 { applied, err := s.AppliedMigrations(ctx) if err != nil { return err } fmt.Printf("%s: already up to date — %d migration(s)\n", c.name, len(applied)) } } return nil } // openInventory connects and waits, the way every command that touches it needs to. func openInventory(ctx context.Context) (*inventory.Inventory, error) { inv, err := inventory.Open(ctx) if err != nil { return nil, err } if err := inv.Ready(ctx, 30*time.Second); err != nil { inv.Close() return nil, err } return inv, nil } func nodeCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("node add , or node list") } inv, err := openInventory(ctx) if err != nil { return err } defer inv.Close() switch args[0] { case "add": if len(args) != 2 { return errors.New("node add ") } node, err := inv.AddNode(ctx, args[1]) if err != nil { return err } fmt.Printf("added %s (%s)\n", node.Name, node.ID) return nil case "list": nodes, err := inv.Nodes(ctx) if err != nil { return err } if len(nodes) == 0 { // Said rather than printed as nothing: an empty list and a failed read must never // look the same, and this command answering "none" is only honest because getting // here means the store answered. fmt.Println("this mesh has no node records yet") return nil } for _, n := range nodes { fmt.Printf("%-20s %s added %s\n", n.Name, n.ID, n.Created.Format(time.RFC3339)) } return nil default: return fmt.Errorf("node has no %q; it has add and list", args[0]) } } func tokenCommand(ctx context.Context, args []string) error { if len(args) == 0 || args[0] != "issue" { return errors.New("token issue --node , or token issue --new ") } set := flag.NewFlagSet("token issue", flag.ContinueOnError) existing := set.String("node", "", "issue for a node record that already exists") fresh := set.String("new", "", "create the node record, then issue for it") validFor := set.Duration("for", time.Hour, "how long the token may be used") if err := set.Parse(args[1:]); err != nil { return err } // Exactly one, because the difference is what the token binds to. A command that guessed // would sometimes create a second record for a machine that already has one. if (*existing == "") == (*fresh == "") { return errors.New("give exactly one of --node or --new : the first is a " + "machine the mesh already has a record for, the second is one it has never seen") } inv, err := openInventory(ctx) if err != nil { return err } defer inv.Close() name := *existing if *fresh != "" { node, err := inv.AddNode(ctx, *fresh) if err != nil { return err } name = node.Name } issued, err := inv.IssueToken(ctx, name, *validFor) if err != nil { return err } fmt.Printf("token for %s, usable once, until %s\n\n %s\n\n", issued.Node.Name, issued.Expires.Format(time.RFC3339), issued.Secret) fmt.Print("This is the only time that secret is shown; what is stored is a hash of it.\n\n") fmt.Print("INCOMPLETE. novox/hq ADR 0004 requires a token to carry four things, and this\n" + "carries one. Missing: the broker's address, the fingerprint of its certificate, and\n" + "the control plane's signing identity. None of the three exists yet, so this secret\n" + "cannot be used to join anything -- it is the half that could be built without them.\n") return nil }