Find a tool by the shell command it replaces, best first (hq ADR 0245)
The agent searched with journalctl, logs and docker ps and found nothing, so it went over ssh. Search now reads what each verb and tool replaces from the controller's records, matches a command line against it, ranks the matches, and says the command it matched.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/announce"
|
||||
)
|
||||
|
||||
// meshToSearch is a mesh as discovery and the controller's records describe it: the service manager's
|
||||
// verbs on two machines, the controller's own, a docker module with its tools, and what each replaces
|
||||
// as the records say it — the seats' from `tools`, the module's from `modules`.
|
||||
func meshToSearch(t *testing.T) *index {
|
||||
t.Helper()
|
||||
var endpoints []announce.Endpoint
|
||||
verbs := map[string]string{
|
||||
"units": "The units the service manager knows in a scope, each with its load, active and sub state.",
|
||||
"status": "One unit as the service manager sees it now: its states, whether it starts at boot.",
|
||||
"restart": "Restart one unit.",
|
||||
"journal": "The last lines of one unit's journal (at most 2000).",
|
||||
}
|
||||
for _, node := range []string{"anchor", "laptop"} {
|
||||
for verb, does := range verbs {
|
||||
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "systemd", Seat: "node-service-manager",
|
||||
Tool: verb, Scope: "node", Node: node, Description: does,
|
||||
Subject: "mesh.seat.node-service-manager.tool." + verb + "." + node})
|
||||
}
|
||||
}
|
||||
for verb, does := range map[string]string{
|
||||
"node": "What one machine reported it can do, what it is assigned, and why.",
|
||||
"nodes": "Every machine the mesh knows, with whether it is converged or adopted.",
|
||||
} {
|
||||
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-controller", Seat: "mesh-controller",
|
||||
Tool: verb, Scope: "mesh", Description: does, Subject: "mesh.seat.mesh-controller.tool." + verb})
|
||||
}
|
||||
for tool, does := range map[string]string{
|
||||
"docker_list": "Every container on this machine: name, image, state, ports.",
|
||||
"docker_logs": "The last lines a container wrote.",
|
||||
"docker_secrets_in_logs": "Whether a container's logs hold a secret.",
|
||||
"docker_restart": "Restart one container.",
|
||||
} {
|
||||
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindTool, Module: "docker", Tool: tool, Node: "anchor",
|
||||
Description: does, Subject: "mesh.mod.docker.tool." + tool + ".anchor"})
|
||||
}
|
||||
x, _, _ := indexOfEndpoints(endpoints)
|
||||
tools := json.RawMessage(`{"seats":[{"seat":"node-service-manager","scope":"node","tools":[
|
||||
{"name":"units","replaces":["systemctl list-units","systemctl --failed"]},
|
||||
{"name":"status","replaces":["systemctl status","systemctl is-active"]},
|
||||
{"name":"restart","replaces":["systemctl restart"]},
|
||||
{"name":"journal","replaces":["journalctl"]}]},
|
||||
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","replaces":["hostnamectl","uptime"]},{"name":"nodes"}]}]}`)
|
||||
x.Replaces = replacesOf(tools, []recordedModule{{Module: "docker", On: []string{"anchor"}, Tools: true,
|
||||
Replaces: map[string][]string{"docker_list": {"docker ps"}, "docker_logs": {"docker logs"}, "docker_restart": {"docker restart"}}}})
|
||||
return x
|
||||
}
|
||||
|
||||
// **The words an agent has find the verb first** (novox/hq ADR 0242): the command it would have run, or
|
||||
// the word it would have used, puts the mesh's tool for it at the top.
|
||||
func TestSearchFindsTheToolThatReplacesACommandFirst(t *testing.T) {
|
||||
x := meshToSearch(t)
|
||||
for query, want := range map[string]string{
|
||||
"journalctl": "anchor/node-service-manager.journal",
|
||||
"journalctl -u mesh-controller -n 50": "anchor/node-service-manager.journal",
|
||||
"sudo journalctl -fu sshd": "anchor/node-service-manager.journal",
|
||||
"/usr/bin/journalctl": "anchor/node-service-manager.journal",
|
||||
"logs": "anchor/node-service-manager.journal",
|
||||
"systemctl status": "anchor/node-service-manager.status",
|
||||
"systemctl status nats.service": "anchor/node-service-manager.status",
|
||||
"systemctl restart mesh-controller": "anchor/node-service-manager.restart",
|
||||
"systemctl --failed": "anchor/node-service-manager.units",
|
||||
"docker ps": "anchor/docker.docker_list",
|
||||
"docker ps -a": "anchor/docker.docker_list",
|
||||
"docker logs": "anchor/docker.docker_logs",
|
||||
"docker logs --tail 100 nats": "anchor/docker.docker_logs",
|
||||
"docker restart nats": "anchor/docker.docker_restart",
|
||||
"container logs": "anchor/docker.docker_logs",
|
||||
"uptime": "mesh-controller.node",
|
||||
} {
|
||||
hits := x.search(query)
|
||||
if len(hits) == 0 {
|
||||
t.Errorf("%q found nothing; want %s first", query, want)
|
||||
continue
|
||||
}
|
||||
if hits[0].Address != want {
|
||||
var got []string
|
||||
for _, h := range hits {
|
||||
got = append(got, h.Address)
|
||||
}
|
||||
t.Errorf("%q found %s first; want %s (all: %s)", query, hits[0].Address, want, strings.Join(got, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A match on a replaced command says which command it replaced, and the most specific one wins.
|
||||
func TestASearchByCommandSaysWhatItReplaces(t *testing.T) {
|
||||
x := meshToSearch(t)
|
||||
hits := x.search("systemctl status sshd")
|
||||
if hits[0].InsteadOf != "systemctl status" {
|
||||
t.Errorf("the match does not say what it replaces: %+v", hits[0])
|
||||
}
|
||||
if hits[0].Also != "also on laptop" {
|
||||
t.Errorf("a node seat's verb does not say its other machines: %+v", hits[0])
|
||||
}
|
||||
// `logs` also finds the container's logs, after the journal — both are answers.
|
||||
var docker bool
|
||||
for _, h := range x.search("logs")[:3] {
|
||||
docker = docker || h.Address == "anchor/docker.docker_logs"
|
||||
}
|
||||
if !docker {
|
||||
t.Errorf("`logs` does not find docker_logs among the first: %+v", x.search("logs"))
|
||||
}
|
||||
}
|
||||
|
||||
// What search did before stays: every word in a name or description, and nothing for a word nothing has.
|
||||
// A synonym is a word, never part of one: `logs` matches through `log`, and finds no `login`.
|
||||
func TestSearchStillMatchesEveryWord(t *testing.T) {
|
||||
x := meshToSearch(t)
|
||||
if hits := x.search("machine converged"); len(hits) != 1 || hits[0].Address != "mesh-controller.nodes" {
|
||||
t.Errorf("words in a description: %+v", hits)
|
||||
}
|
||||
if hits := x.search("journalctl kubernetes"); len(hits) != 1 || hits[0].Address != "anchor/node-service-manager.journal" {
|
||||
t.Errorf("a command with arguments nothing has is still that command: %+v", hits)
|
||||
}
|
||||
if hits := x.search("kubernetes"); len(hits) != 0 {
|
||||
t.Errorf("a word nothing has matched: %+v", hits)
|
||||
}
|
||||
x.Modules["shell"] = &moduleInfo{Module: "shell", On: []string{"anchor"}, Tools: []Tool{{Module: "shell", Name: "shell_login",
|
||||
Description: "The account's login shell."}}}
|
||||
for _, h := range x.search("logs") {
|
||||
if h.Address == "anchor/shell.shell_login" {
|
||||
t.Errorf("`logs` found `login` through its synonym `log`: %+v", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A controller older than `replaces` answers without it: search matches names and descriptions as before.
|
||||
func TestSearchWithoutReplacesMatchesAsBefore(t *testing.T) {
|
||||
x := meshToSearch(t)
|
||||
x.Replaces = replacesOf(json.RawMessage(`{"seats":[{"seat":"node-service-manager","tools":[{"name":"journal"}]}]}`),
|
||||
[]recordedModule{{Module: "docker"}})
|
||||
if len(x.Replaces) != 0 {
|
||||
t.Fatalf("replaces from records that have none: %v", x.Replaces)
|
||||
}
|
||||
if hits := x.search("journal"); len(hits) == 0 || hits[0].Address != "anchor/node-service-manager.journal" {
|
||||
t.Errorf("a verb's own name: %+v", hits)
|
||||
}
|
||||
if hits := x.search("docker logs"); len(hits) == 0 || hits[0].Address != "anchor/docker.docker_logs" {
|
||||
t.Errorf("a tool's own name: %+v", hits)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user