package main import ( "context" "errors" "flag" "fmt" "strings" "time" "github.com/novox/mesh-control/internal/broker" "github.com/novox/mesh-control/internal/inventory" "github.com/novox/mesh-control/internal/link" "github.com/novox/mesh-control/internal/token" ) // what a machine is, and what it is allowed to be told. // // 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. func nodeCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("node add , node list, node show , or " + publicDomainUsage) } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory switch args[0] { case "show": if len(args) != 2 { return errors.New("node show ") } return showNode(ctx, inv, args[1]) 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 %-14s %s\n", n.Name, heardFrom(n), n.ID) } return nil case "public-domain": // The domain this node composes its routed names under (novox/hq ADR 0066). // // **The form with no argument reports; clearing is asked for by name.** It used to clear — // so `node public-domain anchor`, which reads like a question and is what anybody types to // find out what the answer is, silently took every routed name the node had. A read-shaped // invocation must never be a destructive write: there is no output that makes up for it, // because the damage is already done by the time it prints. return publicDomain(ctx, inv, args[1:]) default: return fmt.Errorf("node has no %q; it has add, list, show and public-domain", args[0]) } } // publicDomainUsage is the one description of the three forms, so a refusal and the help agree. const publicDomainUsage = "node public-domain — what it is now; " + " to set it; --clear to take it away" // publicDomain reads, sets or clears the domain a node composes its routed names under. // // Three forms, and the destructive one is the only one that has to be asked for. Clearing is a // real thing to want — a machine that stops facing the outside composes no names, and // lab-versus-production is this one setting (novox/hq ADR 0066) — so it keeps a way to say it. // What it does not keep is being the thing that happens when nothing was said at all. func publicDomain(ctx context.Context, inv *inventory.Inventory, args []string) error { set := flag.NewFlagSet("node public-domain", flag.ContinueOnError) clear := set.Bool("clear", false, "take the domain away; it composes no routed names after") positionals, err := parseAround(set, args) if err != nil { return err } if len(positionals) == 0 || len(positionals) > 2 { return errors.New(publicDomainUsage) } node := positionals[0] switch { case *clear && len(positionals) == 2: // Both, which cannot be meant. Refused rather than one of them silently winning. return fmt.Errorf("give %s a domain or --clear, not both: %q and --clear say opposite "+ "things and the mesh will not choose between them", node, positionals[1]) case *clear: if err := inv.SetPublicDomain(ctx, node, ""); err != nil { return err } fmt.Printf("%s has no public domain, so it composes no routed names\n", node) fmt.Printf(" run `push %s` to take them off it\n", node) return nil case len(positionals) == 2: if err := inv.SetPublicDomain(ctx, node, positionals[1]); err != nil { return err } fmt.Printf("%s composes its routed names under %s\n", node, positionals[1]) fmt.Printf(" run `push %s` to send it\n", node) return nil default: // Asked, so answered. NodeByName first, so a name the mesh has never heard of is a refusal // rather than "it has no public domain", which is true of that name and says nothing. if _, err := inv.NodeByName(ctx, node); err != nil { return err } domain, err := inv.PublicDomainOf(ctx, node) if err != nil { return err } if domain == "" { fmt.Printf("%s has no public domain, so it composes no routed names\n", node) fmt.Printf(" `node public-domain %s ` gives it one\n", node) return nil } fmt.Printf("%s composes its routed names under %s\n", node, domain) return nil } } 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") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory 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 } // Assembled from two contexts by the process that holds both grants. Neither reads the // other's store (novox/hq ADR 0008) — each is asked for its own part. ident, err := openIdentity(ctx) if err != nil { return err } defer ident.Close() key, err := ident.Establish(ctx) if err != nil { return err } // The account is created before the token is handed over, which is what removes the // chicken-and-egg entirely: the mesh runs the broker, so a joining node's credentials can // exist before it does. The one-time secret IS the password, so a node's first connection is // already authenticated and enrolment is what happens over it. if management, err := broker.ManagementFromEnvironment(); err == nil { if err := management.CreateNodeAccount(ctx, issued.Node.Name, issued.Secret); err != nil { return err } fmt.Printf("broker account %s created, scoped to %s and the %s exchange\n\n", issued.Node.Name, link.QueueFor(issued.Node.Name), link.Exchange) } else if !errors.Is(err, broker.ErrNotConfigured) { return err } made := token.Token{Node: issued.Node.Name, Signer: key.Public, Secret: issued.Secret} // Absent is a state, not a failure: a control plane can hold records and a key before it has // a broker. What it cannot do is issue a token anybody could use, and Missing() says so. known, err := broker.FromEnvironment() switch { case err == nil: made.Broker, made.Fingerprint = known.Address, known.Fingerprint case errors.Is(err, broker.ErrNotConfigured): default: return err } encoded, err := made.Encode() 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), encoded) fmt.Println("This is the only time it is shown. What is stored is a hash of the secret.") if missing := made.Missing(); len(missing) > 0 { fmt.Printf("\nINCOMPLETE — this token cannot be used to join anything yet. Missing:\n") for _, m := range missing { fmt.Printf(" - %s\n", m) } fmt.Printf("\nSet %s and %s once the broker is raised.\n", broker.AddressVar, broker.CertificateVar) } return nil } func identityCommand(ctx context.Context, args []string) error { if len(args) == 0 || args[0] != "show" { return errors.New("identity show") } ident, err := openIdentity(ctx) if err != nil { return err } defer ident.Close() // Establish rather than read: a control plane asked for its identity before it has one should // get one, not an error. Generating it is idempotent, so this is safe to run at any time. key, err := ident.Establish(ctx) if err != nil { return err } fmt.Printf("signing key %s\n", key.ID) fmt.Printf("fingerprint %s\n", key.Fingerprint()) fmt.Printf("created %s\n", key.Created.Format(time.RFC3339)) fmt.Printf("\nThe public half of this travels in every enrolment token. A node believes a\n" + "declaration because it carries a signature this key made (novox/hq ADR 0004).\n") return nil } func brokerCommand(args []string) error { if len(args) == 0 || args[0] != "show" { return errors.New("broker show") } known, err := broker.FromEnvironment() if errors.Is(err, broker.ErrNotConfigured) { fmt.Printf("no broker configured. Set %s and %s.\n\n"+ "Until then tokens carry the signing key and the one-time secret, and say what they\n"+ "are missing. They cannot be used to join.\n", broker.AddressVar, broker.CertificateVar) return nil } if err != nil { return err } fmt.Printf("address %s\n", known.Address) fmt.Printf("fingerprint %s\n", known.Fingerprint) fmt.Print("\nThe fingerprint is computed from the certificate on disk, never configured. A\n" + "node checks it before sending anything (novox/hq ADR 0004).\n") return nil } // heardFrom says when a node was last heard from, in a form somebody can act on. // // "never" and "an hour ago" are different answers and are kept different. A node that has never // spoken did not finish joining; a node last heard from an hour ago is running an hour-old // picture of the mesh. func heardFrom(n inventory.Node) string { silent, ever := n.Silent() switch { case !ever: return "never spoken" case silent > SilentFor: return "out of touch " + roughly(silent) default: return "here" } } // roughly is a duration a person reads rather than parses. func roughly(d time.Duration) string { switch { case d < time.Hour: return fmt.Sprintf("%dm", int(d.Minutes())) case d < 48*time.Hour: return fmt.Sprintf("%dh", int(d.Hours())) default: return fmt.Sprintf("%dd", int(d.Hours()/24)) } } // showNode says what one machine reported about itself, in its own words. // // **A capability is detected and never assumed** (novox/hq ADR 0009), so the only account of what // a machine can do is the one it gave — and its detail is half of that account. The mesh was // keeping the yes and discarding the reason, which makes *this machine has no seat* an answer with // nowhere to go: a person told a machine lacks something wants to know what the detector saw. // // It is also where "what should it be configured as" is read. The same line that gates an // assignment carries `card1-DP-1`, and a person composing settings for that machine needs it. func showNode(ctx context.Context, inv *inventory.Inventory, name string) error { node, err := inv.NodeByName(ctx, name) if err != nil { return err } fmt.Printf("%s\n", node.Name) fmt.Printf(" last heard from %s\n", heardFrom(node)) // The domain its routed names are composed under, when it has one (novox/hq ADR 0066). Shown // only when set: a machine that serves nothing to the outside has no domain, and saying so of // every internal node would be noise. domain, err := inv.PublicDomainOf(ctx, name) if err != nil { return err } if domain != "" { fmt.Printf(" public domain %s\n", domain) } held, err := inv.Profile(ctx, name) if err != nil { return err } if held == nil { // Never reported is not the same as reported nothing, and the remedy differs: one is a // machine that has not run the host yet, the other is a machine that ran it and can do // nothing. fmt.Printf("\n this machine has never said what it can do, so everything requiring a\n" + " capability is refused here — run the host on it\n") return nil } if len(held) == 0 { fmt.Printf("\n it reported no capabilities at all\n") return nil } fmt.Printf("\n what it can do, as it reported:\n") for _, c := range held { mark := "no " if c.Present { mark = "yes" } fmt.Printf(" %s %-20s %s\n", mark, c.Name, c.Detail) } assigned, err := inv.Assigned(ctx, name) if err != nil { return err } if len(assigned) > 0 { fmt.Printf("\n assigned: %s\n", strings.Join(assigned, ", ")) } return nil }