The Go runtime answers $SRV.PING, $SRV.INFO and $SRV.STATS (and per name and id) in the io.nats.micro.v1 format with what it serves at the moment it is asked: one service per runtime process, since the bus admits one reply per request from each responder, and one endpoint per tool per subject, its metadata saying module, seat, scope, machine, description, schema and whether the module is interchangeable. Serving is unchanged. The console gathers one $SRV.INFO request's answers instead of asking the catalogue's roster and each module's tools, and reads the controller's records as JSON for what should have answered: an assignment with tools that did not announce is named, a module without tools never is. The text parsers of node list and module list are gone. Packages share the test bus: go test -p 1.
140 lines
3.7 KiB
Go
140 lines
3.7 KiB
Go
package console
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
)
|
|
|
|
// Tool is a tool as its module — or, for a role's tool, the mesh's records — describes it.
|
|
type Tool struct {
|
|
Module string
|
|
Name string
|
|
Description string
|
|
Input json.RawMessage
|
|
Seat bool
|
|
Scope string
|
|
Subjects []string
|
|
}
|
|
|
|
// Listing is what the mesh could say about its tools; silence is named, never dropped (design 34 §3).
|
|
type Listing struct {
|
|
Tools []Tool
|
|
NotAnswering []string
|
|
}
|
|
|
|
// Seats is the seats and their verbs, so `<seat>.<verb>` resolves to the role.
|
|
type Seats map[string]map[string]bool
|
|
|
|
func seatsIn(l *Listing) Seats {
|
|
out := Seats{}
|
|
for _, t := range l.Tools {
|
|
if !t.Seat {
|
|
continue
|
|
}
|
|
if out[t.Module] == nil {
|
|
out[t.Module] = map[string]bool{}
|
|
}
|
|
out[t.Module][t.Name] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
// toolKey is the key a call uses: a role's when the prefix is a seat declaring that verb.
|
|
func toolKey(name string, seats Seats) string {
|
|
if strings.HasPrefix(name, "seat:") {
|
|
return name
|
|
}
|
|
dot := strings.Index(name, ".")
|
|
if dot < 0 {
|
|
return name
|
|
}
|
|
if seats[name[:dot]][name[dot+1:]] {
|
|
return "seat:" + name
|
|
}
|
|
return name
|
|
}
|
|
|
|
// toolsOn is the flat catalogue (MESH_CONSOLE_FLAT=1): what announced itself on the bus, every
|
|
// tool and seat verb, and the assignments with tools that did not (novox/hq ADR 0197).
|
|
func toolsOn(conn *bus.Conn) (*Listing, error) {
|
|
x, err := indexOn(conn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return x.Listing, nil
|
|
}
|
|
|
|
// callTool calls `<module>.<tool>[@<node>]`, on the subject the listing names for it when it names one.
|
|
func callTool(conn *bus.Conn, key string, args any, seats Seats, l *Listing) (bus.Answered, error) {
|
|
name, node, _ := strings.Cut(key, "@")
|
|
if !strings.Contains(name, ".") {
|
|
return bus.Answered{}, fmt.Errorf("%q does not name a tool: write <module>.<tool>, as `mesh tools` lists "+
|
|
"them, or <module>.<tool>@<node> for the instance on one machine", key)
|
|
}
|
|
resolved := toolKey(name, seats)
|
|
if node != "" {
|
|
resolved += "@" + node
|
|
}
|
|
return conn.Ask(resolved, args, subjectListed(name, node, l))
|
|
}
|
|
|
|
func subjectListed(name, node string, l *Listing) string {
|
|
if l == nil {
|
|
return ""
|
|
}
|
|
dot := strings.Index(name, ".")
|
|
module, tool := name[:dot], name[dot+1:]
|
|
for _, t := range l.Tools {
|
|
if t.Module == module && t.Name == tool && !t.Seat {
|
|
if len(t.Subjects) == 0 {
|
|
return ""
|
|
}
|
|
if node == "" {
|
|
return t.Subjects[0]
|
|
}
|
|
for _, s := range t.Subjects {
|
|
if strings.HasSuffix(s, "."+node) {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var (
|
|
noResponders = regexp.MustCompile(`(?i)no responders|503`)
|
|
refused = regexp.MustCompile(`(?i)permissions violation|authorization`)
|
|
timedOut = regexp.MustCompile(`(?i)timeout`)
|
|
)
|
|
|
|
// whyItFailed says why a call failed, so the remedy is in the words.
|
|
func whyItFailed(key string, err error) string {
|
|
if err == nil {
|
|
err = errors.New("failed")
|
|
}
|
|
msg := err.Error()
|
|
switch {
|
|
case noResponders.MatchString(msg):
|
|
extra := ""
|
|
if strings.HasPrefix(key, "seat:") {
|
|
extra = ", or nothing holds that seat"
|
|
}
|
|
return "nothing serves " + key + ". The module may not be assigned to any machine, or it is down" +
|
|
extra + " — `mesh tools` lists what answered."
|
|
case refused.MatchString(msg):
|
|
return "this account may not call " + key + ". What it may call was fixed when it was issued — a " +
|
|
"person's by `operator issue`, the console's by its manifest."
|
|
case timedOut.MatchString(msg):
|
|
return key + " did not answer in time. Something is serving it, so this is the tool being slow " +
|
|
"rather than absent."
|
|
}
|
|
return key + " failed: " + msg
|
|
}
|