"authorization" alone matched a tool's own answer mentioning the word — the runtime refusing a state value with an Authorization header read as "this account may not call".
143 lines
4.0 KiB
Go
143 lines
4.0 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`)
|
|
// The bus's own two refusals, by their whole phrase. "authorization" alone also matched a tool's own
|
|
// answer that merely mentions the word — the runtime refusing a state value with an Authorization
|
|
// header (novox/hq ADR 0201) read as "this account may not call", which sent the reader the wrong way.
|
|
refused = regexp.MustCompile(`(?i)permissions violation|authorization violation`)
|
|
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
|
|
}
|