Say what each verb replaces, so the agent is pointed at it instead of a shell command (hq ADR 0245)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
A seat's verb names the shell commands it is the mesh's way to do, and a module says it for its own tools in its manifest; the tools verb and module list --json carry both, for the mesh MCP server's search, the agent's instructions and the guard on its shell.
This commit is contained in:
@@ -182,6 +182,9 @@ func moduleCommand(ctx context.Context, args []string) error {
|
|||||||
Requires []string `json:"requires,omitempty"`
|
Requires []string `json:"requires,omitempty"`
|
||||||
Claims []string `json:"claims,omitempty"`
|
Claims []string `json:"claims,omitempty"`
|
||||||
Capabilities []string `json:"capabilities,omitempty"`
|
Capabilities []string `json:"capabilities,omitempty"`
|
||||||
|
// Replaces is what each of its own tools replaces (novox/hq ADR 0245), for the console's
|
||||||
|
// search and the agent's instructions.
|
||||||
|
Replaces map[string][]string `json:"replaces,omitempty"`
|
||||||
}
|
}
|
||||||
out := make([]listed, 0, len(entries))
|
out := make([]listed, 0, len(entries))
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -189,7 +192,7 @@ func moduleCommand(ctx context.Context, args []string) error {
|
|||||||
l := listed{Module: m.Module, Version: m.Version, Built: e.Source.BuiltFrom, Head: e.Source.Head,
|
l := listed{Module: m.Module, Version: m.Version, Built: e.Source.BuiltFrom, Head: e.Source.Head,
|
||||||
Current: e.Provided || e.Source.Repository == "" || e.Source.Current(), Provided: e.Provided,
|
Current: e.Provided || e.Source.Repository == "" || e.Source.Current(), Provided: e.Provided,
|
||||||
On: append([]string{}, e.On...), Provides: m.Offers(), Requires: m.Requires,
|
On: append([]string{}, e.On...), Provides: m.Offers(), Requires: m.Requires,
|
||||||
Capabilities: m.Capabilities, Tools: declaresTools(m)}
|
Capabilities: m.Capabilities, Tools: declaresTools(m), Replaces: m.Replaces}
|
||||||
for _, c := range m.Claims {
|
for _, c := range m.Claims {
|
||||||
l.Claims = append(l.Claims, c.At()+"/"+c.Name)
|
l.Claims = append(l.Claims, c.At()+"/"+c.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A recorded module is composed at the build this machine runs, on any send but a person's push
|
// A recorded module is composed at the build this machine runs, on any send but a person's push
|
||||||
// (novox/hq issue 295, ADR 0242).
|
// (novox/hq issue 295, ADR 0245).
|
||||||
if _, err := keepRecorded(ctx, open, nodeName, shelf); err != nil {
|
if _, err := keepRecorded(ctx, open, nodeName, shelf); err != nil {
|
||||||
return catalogue.Resolution{}, nil, err
|
return catalogue.Resolution{}, nil, err
|
||||||
}
|
}
|
||||||
@@ -439,7 +439,7 @@ func declarationWith(ctx context.Context, open *stores, node string,
|
|||||||
names = append(names, m.Module)
|
names = append(names, m.Module)
|
||||||
}
|
}
|
||||||
out.Builds = carriedBuilds(names, composed.LeftOut, current, before)
|
out.Builds = carriedBuilds(names, composed.LeftOut, current, before)
|
||||||
// A recorded module kept at the build the machine runs is recorded as carrying that one (ADR 0242).
|
// A recorded module kept at the build the machine runs is recorded as carrying that one (ADR 0245).
|
||||||
kept, err := recordedKept(ctx, open, node)
|
kept, err := recordedKept(ctx, open, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendable{}, err
|
return sendable{}, err
|
||||||
|
|||||||
@@ -1082,7 +1082,7 @@ func sendToEach(ctx context.Context, open *stores, names []string) ([]string, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// **A recorded build moves only by a person's push** (novox/hq issue 295, ADR 0242): this send — a
|
// **A recorded build moves only by a person's push** (novox/hq issue 295, ADR 0245): this send — a
|
||||||
// plan's, a release plan's, a rollback's, a healer's, a rotation's — composes every recorded module at
|
// plan's, a release plan's, a rollback's, a healer's, a rotation's — composes every recorded module at
|
||||||
// the build its machine runs. The bus step is a person's word for the bus alone.
|
// the build its machine runs. The bus step is a person's word for the bus alone.
|
||||||
if ctx, err = sendKeeps(ctx, inv); err != nil {
|
if ctx, err = sendKeeps(ctx, inv); err != nil {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A recorded build reaches a machine only by a person's push (novox/hq issue 295, ADR 0242).
|
// A recorded build reaches a machine only by a person's push (novox/hq issue 295, ADR 0245).
|
||||||
//
|
//
|
||||||
// **A send carries the machine's whole declaration** (ADR 0221), composed from the build the mesh holds
|
// **A send carries the machine's whole declaration** (ADR 0221), composed from the build the mesh holds
|
||||||
// of every module on it. A module whose upgrade policy records — postgres, mongodb, keycloak, the
|
// of every module on it. A module whose upgrade policy records — postgres, mongodb, keycloak, the
|
||||||
@@ -121,7 +121,7 @@ func keepRecorded(ctx context.Context, open *stores, node string, shelf map[stri
|
|||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+
|
return nil, fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+
|
||||||
"records no longer hold: this send cannot keep it and does not move it — `push %s` sends the new "+
|
"records no longer hold: this send cannot keep it and does not move it — `push %s` sends the new "+
|
||||||
"one on a person's word (novox/hq ADR 0242)", m, node, short(kept[m]), node)
|
"one on a person's word (novox/hq ADR 0245)", m, node, short(kept[m]), node)
|
||||||
}
|
}
|
||||||
shelf[m] = ran
|
shelf[m] = ran
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A recorded build reaches a machine only by a person's push (novox/hq issue 295, ADR 0242).
|
// A recorded build reaches a machine only by a person's push (novox/hq issue 295, ADR 0245).
|
||||||
|
|
||||||
// aContainerBuild is a build outcome of a module of containers, each named by id with the image and the
|
// aContainerBuild is a build outcome of a module of containers, each named by id with the image and the
|
||||||
// health it is given; a policy when one is said.
|
// health it is given; a policy when one is said.
|
||||||
@@ -189,7 +189,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The send says what it recreates (ADR 0242): mail's health checks adopted recreate both its
|
// The send says what it recreates (ADR 0245): mail's health checks adopted recreate both its
|
||||||
// containers, with no new image, every one at once.
|
// containers, with no new image, every one at once.
|
||||||
func TestASendSaysWhatItRecreates(t *testing.T) {
|
func TestASendSaysWhatItRecreates(t *testing.T) {
|
||||||
image := "registry.invalid:5000/mailu/smtp@sha256:" + strings.Repeat("c", 64)
|
image := "registry.invalid:5000/mailu/smtp@sha256:" + strings.Repeat("c", 64)
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ func backlogCommand(ctx context.Context, sub string, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sayRecreations says, on each move a send carries, what it does to the module's containers (novox/hq
|
// sayRecreations says, on each move a send carries, what it does to the module's containers (novox/hq
|
||||||
// ADR 0242): the build the machine ran against the one it is sent, container by container. Left unsaid
|
// ADR 0245): the build the machine ran against the one it is sent, container by container. Left unsaid
|
||||||
// for a move whose earlier build is not in the records.
|
// for a move whose earlier build is not in the records.
|
||||||
func sayRecreations(ctx context.Context, open *stores, moves []inventory.CarriedMove) {
|
func sayRecreations(ctx context.Context, open *stores, moves []inventory.CarriedMove) {
|
||||||
if len(moves) == 0 {
|
if len(moves) == 0 {
|
||||||
|
|||||||
@@ -904,7 +904,7 @@ func firstSend(ctx context.Context, open *stores, p *inventory.Plan, node string
|
|||||||
strings.Join(sent, ", "))
|
strings.Join(sent, ", "))
|
||||||
fmt.Printf("%s: tier %d built; sent %d module(s) to %s first in one send (%s), the rest once its gate passes\n",
|
fmt.Printf("%s: tier %d built; sent %d module(s) to %s first in one send (%s), the rest once its gate passes\n",
|
||||||
p.ID, p.Tier, len(modules), strings.Join(sent, ", "), strings.Join(modules, ", "))
|
p.ID, p.Tier, len(modules), strings.Join(sent, ", "), strings.Join(modules, ", "))
|
||||||
// What the send recreates, said with it (novox/hq ADR 0242).
|
// What the send recreates, said with it (novox/hq ADR 0245).
|
||||||
if said := recreationsSaid(carried); said != "" {
|
if said := recreationsSaid(carried); said != "" {
|
||||||
p.Note += "; " + said
|
p.Note += "; " + said
|
||||||
fmt.Printf("%s: %s\n", p.ID, said)
|
fmt.Printf("%s: %s\n", p.ID, said)
|
||||||
@@ -1287,7 +1287,7 @@ func plansCommand(ctx context.Context, args []string) error {
|
|||||||
case s != nil && s.Gate != nil:
|
case s != nil && s.Gate != nil:
|
||||||
fmt.Printf(" %-22s %s\n", "", gateLine(s.Gate))
|
fmt.Printf(" %-22s %s\n", "", gateLine(s.Gate))
|
||||||
}
|
}
|
||||||
// What the send to its first machine did to its containers (ADR 0242).
|
// What the send to its first machine did to its containers (ADR 0245).
|
||||||
if s != nil && s.Gate != nil {
|
if s != nil && s.Gate != nil {
|
||||||
for _, c := range s.Gate.Carried {
|
for _, c := range s.Gate.Carried {
|
||||||
if c.Recreates != "" {
|
if c.Recreates != "" {
|
||||||
|
|||||||
@@ -970,9 +970,15 @@ func seatTools() map[string]any {
|
|||||||
}
|
}
|
||||||
var tools []map[string]any
|
var tools []map[string]any
|
||||||
for _, v := range s.Serves {
|
for _, v := range s.Serves {
|
||||||
tools = append(tools, map[string]any{
|
tool := map[string]any{
|
||||||
"name": v.Name, "description": v.Description, "input": v.Input, "output": v.Output,
|
"name": v.Name, "description": v.Description, "input": v.Input, "output": v.Output,
|
||||||
})
|
}
|
||||||
|
// What the verb is the mesh's way to do (novox/hq ADR 0245): read by the console's search and
|
||||||
|
// the agent's instructions.
|
||||||
|
if len(v.Replaces) > 0 {
|
||||||
|
tool["replaces"] = v.Replaces
|
||||||
|
}
|
||||||
|
tools = append(tools, tool)
|
||||||
}
|
}
|
||||||
seats = append(seats, map[string]any{"seat": s.Name, "scope": s.Scope, "tools": tools})
|
seats = append(seats, map[string]any{"seat": s.Name, "scope": s.Scope, "tools": tools})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,23 @@ func TestToolsAnswersTheSeatsRecords(t *testing.T) {
|
|||||||
if !found {
|
if !found {
|
||||||
t.Fatal("the mesh-controller seat is not in the listing")
|
t.Fatal("the mesh-controller seat is not in the listing")
|
||||||
}
|
}
|
||||||
|
// And what a verb replaces travels with it (novox/hq ADR 0245): the console and the agent's
|
||||||
|
// instructions read it from here.
|
||||||
|
var journal []string
|
||||||
|
for _, s := range seats {
|
||||||
|
if s["seat"] != catalogue.ServiceManagerSeat {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tools, _ := s["tools"].([]map[string]any)
|
||||||
|
for _, tool := range tools {
|
||||||
|
if tool["name"] == "journal" {
|
||||||
|
journal, _ = tool["replaces"].([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(journal) == 0 || journal[0] != "journalctl" {
|
||||||
|
t.Fatalf("the service manager's journal does not say it replaces journalctl: %v", journal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A JSON verb's answer is parsed from what the command wrote to standard output alone; a warning it
|
// A JSON verb's answer is parsed from what the command wrote to standard output alone; a warning it
|
||||||
|
|||||||
@@ -426,6 +426,13 @@ type Manifest struct {
|
|||||||
// module claiming a seat answers what that seat's protocol promises (novox/hq ADR 0118).
|
// module claiming a seat answers what that seat's protocol promises (novox/hq ADR 0118).
|
||||||
Tools []string `json:"tools,omitempty"`
|
Tools []string `json:"tools,omitempty"`
|
||||||
|
|
||||||
|
// Replaces says, for a tool of this module's own, the shell commands it is the mesh's way to do —
|
||||||
|
// `{"docker_logs": ["docker logs"]}` — as a seat's verb says it in its definition (novox/hq ADR
|
||||||
|
// 0241). A seat's verb carries its own: what a role replaces is the role's, so a module never says it
|
||||||
|
// for a verb it serves under a claim. The console's search, the agent's instructions and the guard on
|
||||||
|
// its shell are built from both.
|
||||||
|
Replaces map[string][]string `json:"replaces,omitempty"`
|
||||||
|
|
||||||
// Instances says whether this module's instances are the same anywhere — `interchangeable` —
|
// Instances says whether this module's instances are the same anywhere — `interchangeable` —
|
||||||
// so a call that names no machine may be answered by any of them (novox/hq ADR 0160). A fact
|
// so a call that names no machine may be answered by any of them (novox/hq ADR 0160). A fact
|
||||||
// about the software, not about the bus: a stateless web tool says it; a database does not,
|
// about the software, not about the bus: a stateless web tool says it; a database does not,
|
||||||
@@ -1692,6 +1699,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
problems = append(problems, invokeProblems(m)...)
|
problems = append(problems, invokeProblems(m)...)
|
||||||
|
problems = append(problems, replacesProblems(m)...)
|
||||||
problems = append(problems, endpointNameProblems(m)...)
|
problems = append(problems, endpointNameProblems(m)...)
|
||||||
problems = append(problems, RouteProblems(m)...)
|
problems = append(problems, RouteProblems(m)...)
|
||||||
for _, port := range m.Guards {
|
for _, port := range m.Guards {
|
||||||
@@ -2306,6 +2314,53 @@ func EndpointPort(m Manifest, name string) (int, bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxReplaced is the longest command a tool may say it replaces: a command's name and the words that
|
||||||
|
// make it this command, never a script.
|
||||||
|
const MaxReplaced = 80
|
||||||
|
|
||||||
|
// replacesProblems judges what a module says its tools replace (novox/hq ADR 0245): each key a tool it
|
||||||
|
// declares, each entry a command line of one line, short, and said once.
|
||||||
|
func replacesProblems(m Manifest) []string {
|
||||||
|
var problems []string
|
||||||
|
tools := map[string]bool{}
|
||||||
|
for _, t := range m.Tools {
|
||||||
|
tools[t] = true
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(m.Replaces))
|
||||||
|
for k := range m.Replaces {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, tool := range keys {
|
||||||
|
if !tools[tool] {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s says what %q replaces, and declares no such tool: "+
|
||||||
|
"a module says it for a tool in its `tools`; a seat's verb says it in the seat's definition "+
|
||||||
|
"(novox/hq ADR 0245)", m.Module, tool))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(m.Replaces[tool]) == 0 {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s says %s replaces nothing: leave it out", m.Module, tool))
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, c := range m.Replaces[tool] {
|
||||||
|
c = strings.TrimSpace(c)
|
||||||
|
switch {
|
||||||
|
case c == "":
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: %s replaces an empty command", m.Module, tool))
|
||||||
|
case strings.ContainsAny(c, "\n\r"):
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: %s replaces %q, which is more than one line", m.Module, tool, c))
|
||||||
|
case len(c) > MaxReplaced:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: %s replaces %q, longer than %d characters: "+
|
||||||
|
"the command's name and the words that make it this command", m.Module, tool, c, MaxReplaced))
|
||||||
|
case seen[c]:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: %s replaces %q twice", m.Module, tool, c))
|
||||||
|
}
|
||||||
|
seen[c] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
// invokeProblems judges what a module says it calls (novox/hq ADR 0152).
|
// invokeProblems judges what a module says it calls (novox/hq ADR 0152).
|
||||||
//
|
//
|
||||||
// Refused here, in the manifest's words, rather than at the next composition of the bus's user
|
// Refused here, in the manifest's words, rather than at the next composition of the bus's user
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// What a move does to a module's containers (novox/hq ADR 0242).
|
// What a move does to a module's containers (novox/hq ADR 0245).
|
||||||
//
|
//
|
||||||
// A container whose declaration changes is recreated, whatever changed in it: an image, an environment
|
// A container whose declaration changes is recreated, whatever changed in it: an image, an environment
|
||||||
// variable, a health check adopted. A module whose containers are recreated in one send is down while
|
// variable, a health check adopted. A module whose containers are recreated in one send is down while
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The verbs an agent works around most say what they replace (novox/hq ADR 0245): the journal, a
|
||||||
|
// restart, a unit's status, the hosts file — each the command the agent would otherwise type over ssh.
|
||||||
|
func TestTheVerbsWorkedAroundSayWhatTheyReplace(t *testing.T) {
|
||||||
|
want := map[string]string{
|
||||||
|
ServiceManagerSeat + ".journal": "journalctl",
|
||||||
|
ServiceManagerSeat + ".restart": "systemctl restart",
|
||||||
|
ServiceManagerSeat + ".status": "systemctl status",
|
||||||
|
"node-hostname.add": "edit /etc/hosts",
|
||||||
|
ControllerSeatName + ".node": "hostnamectl",
|
||||||
|
}
|
||||||
|
for _, s := range DefaultSeats() {
|
||||||
|
for _, v := range s.Serves {
|
||||||
|
cmd, ok := want[s.Name+"."+v.Name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(want, s.Name+"."+v.Name)
|
||||||
|
found := false
|
||||||
|
for _, r := range v.Replaces {
|
||||||
|
found = found || r == cmd
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("%s.%s does not say it replaces %q: %v", s.Name, v.Name, cmd, v.Replaces)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for verb := range want {
|
||||||
|
t.Errorf("%s is not a verb of the compiled seats", verb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every command a seat's verb replaces is one short line, said once — what the guard and the search match.
|
||||||
|
func TestEveryReplacedCommandIsOneShortLine(t *testing.T) {
|
||||||
|
for _, s := range DefaultSeats() {
|
||||||
|
for _, v := range s.Serves {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, r := range v.Replaces {
|
||||||
|
if strings.TrimSpace(r) == "" || strings.ContainsAny(r, "\n\r") || len(r) > MaxReplaced || seen[r] {
|
||||||
|
t.Errorf("%s.%s replaces %q, which is not one short line said once", s.Name, v.Name, r)
|
||||||
|
}
|
||||||
|
seen[r] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A verb's definition in a manifest may say what it replaces, like the mesh's own.
|
||||||
|
func TestAVerbDefinitionCarriesWhatItReplaces(t *testing.T) {
|
||||||
|
m, err := ParseManifest([]byte(`{"module":"till","version":"1","tools":["refund"],` +
|
||||||
|
`"seats":[{"name":"shop-till","serves":[{"name":"refund","replaces":["psql -c refund"]}]}],` +
|
||||||
|
`"claims":[{"name":"shop-till","scope":"mesh"}]}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := m.DefinesSeats[0].Serves[0].Replaces; len(got) != 1 || got[0] != "psql -c refund" {
|
||||||
|
t.Fatalf("replaces not read: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A module says what its own tools replace, and only for a tool it declares.
|
||||||
|
func TestAModuleSaysWhatItsOwnToolsReplace(t *testing.T) {
|
||||||
|
m, err := ParseManifest([]byte(`{"module":"docker","version":"1","tools":["docker_logs","docker_list"],` +
|
||||||
|
`"replaces":{"docker_logs":["docker logs"],"docker_list":["docker ps"]}}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if m.Replaces["docker_logs"][0] != "docker logs" || m.Replaces["docker_list"][0] != "docker ps" {
|
||||||
|
t.Fatalf("replaces not read: %v", m.Replaces)
|
||||||
|
}
|
||||||
|
for _, c := range []struct{ manifest, says string }{
|
||||||
|
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_lgos":["docker logs"]}}`,
|
||||||
|
"declares no such tool"},
|
||||||
|
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":[]}}`, "replaces nothing"},
|
||||||
|
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":[" "]}}`, "empty command"},
|
||||||
|
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs\nrm -rf /"]}}`,
|
||||||
|
"more than one line"},
|
||||||
|
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs","docker logs"]}}`,
|
||||||
|
"twice"},
|
||||||
|
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["` + strings.Repeat("x", MaxReplaced+1) + `"]}}`,
|
||||||
|
"longer than"},
|
||||||
|
} {
|
||||||
|
if _, err := ParseManifest([]byte(c.manifest)); err == nil || !strings.Contains(err.Error(), c.says) {
|
||||||
|
t.Errorf("%s: want a refusal saying %q, got %v", c.manifest, c.says, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A seat row the store seeded before `replaces` existed carries none; the working set takes the compiled
|
||||||
|
// one, so the `tools` answer says it on a mesh whose rows exist.
|
||||||
|
func TestASeededRowTakesWhatItsVerbsReplaceFromTheCompiledSeat(t *testing.T) {
|
||||||
|
was := Seats()
|
||||||
|
t.Cleanup(func() { UseSeats(was) })
|
||||||
|
var row Seat
|
||||||
|
for _, s := range DefaultSeats() {
|
||||||
|
if s.Name == ServiceManagerSeat {
|
||||||
|
row = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stored := make([]Verb, len(row.Serves))
|
||||||
|
for i, v := range row.Serves {
|
||||||
|
v.Replaces = nil
|
||||||
|
stored[i] = v
|
||||||
|
}
|
||||||
|
row.Serves = stored
|
||||||
|
UseSeats([]Seat{row})
|
||||||
|
got, _ := SeatNamed(ServiceManagerSeat)
|
||||||
|
for _, v := range got.Serves {
|
||||||
|
if v.Name == "journal" && (len(v.Replaces) == 0 || v.Replaces[0] != "journalctl") {
|
||||||
|
t.Fatalf("the seeded journal verb says it replaces %v", v.Replaces)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
-25
@@ -102,10 +102,10 @@ var defaultSeats = append([]Seat{
|
|||||||
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079",
|
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079",
|
||||||
Serves: []Verb{
|
Serves: []Verb{
|
||||||
{Name: "databases", Description: "Every database the store holds, with its on-disk size.",
|
{Name: "databases", Description: "Every database the store holds, with its on-disk size.",
|
||||||
Input: schema(map[string]string{}, nil)},
|
Input: schema(map[string]string{}, nil), Replaces: []string{"psql -l"}},
|
||||||
{Name: "query", Description: "One read-only statement against one database the store holds.",
|
{Name: "query", Description: "One read-only statement against one database the store holds.",
|
||||||
Input: schema(map[string]string{"database": "the database to query", "sql": "the read-only statement"},
|
Input: schema(map[string]string{"database": "the database to query", "sql": "the read-only statement"},
|
||||||
[]string{"database", "sql"})},
|
[]string{"database", "sql"}), Replaces: []string{"psql"}},
|
||||||
}},
|
}},
|
||||||
// **Delivers the mesh's own bus, not `amqp`.** Those were the same word until
|
// **Delivers the mesh's own bus, not `amqp`.** Those were the same word until
|
||||||
// ADR 0127 separated them: `amqp` is a backing service a module may require, and this seat is
|
// ADR 0127 separated them: `amqp` is a backing service a module may require, and this seat is
|
||||||
@@ -174,15 +174,16 @@ var defaultSeats = append([]Seat{
|
|||||||
Serves: []Verb{
|
Serves: []Verb{
|
||||||
{Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: " +
|
{Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: " +
|
||||||
"the operator's, or the block of the module or tool that writes it.",
|
"the operator's, or the block of the module or tool that writes it.",
|
||||||
Input: schema(map[string]string{}, nil)},
|
Input: schema(map[string]string{}, nil), Replaces: []string{"cat /etc/hosts", "getent hosts"}},
|
||||||
{Name: "add", Description: "Add one address and its names to the operator's lines of this machine's " +
|
{Name: "add", Description: "Add one address and its names to the operator's lines of this machine's " +
|
||||||
"/etc/hosts — a name for this machine's own programs, not the mesh's.",
|
"/etc/hosts — a name for this machine's own programs, not the mesh's.",
|
||||||
Input: schema(map[string]string{"address": "the IPv4 or IPv6 address",
|
Input: schema(map[string]string{"address": "the IPv4 or IPv6 address",
|
||||||
"names": "the names for it, separated by spaces"}, []string{"address", "names"})},
|
"names": "the names for it, separated by spaces"}, []string{"address", "names"}),
|
||||||
|
Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}},
|
||||||
{Name: "remove", Description: "Remove one name, or every line of one address, from the operator's " +
|
{Name: "remove", Description: "Remove one name, or every line of one address, from the operator's " +
|
||||||
"lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.",
|
"lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.",
|
||||||
Input: schema(map[string]string{"name": "a host name, or an address to remove every line of"},
|
Input: schema(map[string]string{"name": "a host name, or an address to remove every line of"},
|
||||||
[]string{"name"})},
|
[]string{"name"}), Replaces: []string{"sed -i /etc/hosts"}},
|
||||||
}},
|
}},
|
||||||
// The intrusion prevention's verbs (novox/hq ADR 0179): what a person asks a machine's ban list
|
// The intrusion prevention's verbs (novox/hq ADR 0179): what a person asks a machine's ban list
|
||||||
// whatever keeps it — who is banned and why, ban one address, let one go. Every holder serves all
|
// whatever keeps it — who is banned and why, ban one address, let one go. Every holder serves all
|
||||||
@@ -191,15 +192,17 @@ var defaultSeats = append([]Seat{
|
|||||||
Serves: []Verb{
|
Serves: []Verb{
|
||||||
{Name: "status", Description: "Every jail on this machine with how many it is watching and " +
|
{Name: "status", Description: "Every jail on this machine with how many it is watching and " +
|
||||||
"holding now, and the totals since the jail started; one jail's detail when named.",
|
"holding now, and the totals since the jail started; one jail's detail when named.",
|
||||||
Input: schema(map[string]string{"jail": "one jail (optional)"}, nil)},
|
Input: schema(map[string]string{"jail": "one jail (optional)"}, nil), Replaces: []string{"fail2ban-client status"}},
|
||||||
{Name: "banned", Description: "Every address banned on this machine right now, with the jail " +
|
{Name: "banned", Description: "Every address banned on this machine right now, with the jail " +
|
||||||
"that holds it and when the ban ends.",
|
"that holds it and when the ban ends.",
|
||||||
Input: schema(map[string]string{"jail": "one jail (optional)"}, nil)},
|
Input: schema(map[string]string{"jail": "one jail (optional)"}, nil), Replaces: []string{"fail2ban-client banned"}},
|
||||||
{Name: "ban", Description: "Ban one address in one jail now, for the jail's ban time — an " +
|
{Name: "ban", Description: "Ban one address in one jail now, for the jail's ban time — an " +
|
||||||
"operator's act on the live ban list, which the mesh never writes itself.",
|
"operator's act on the live ban list, which the mesh never writes itself.",
|
||||||
Input: schema(map[string]string{"ip": "the address", "jail": "the jail to hold it"}, []string{"ip", "jail"})},
|
Input: schema(map[string]string{"ip": "the address", "jail": "the jail to hold it"}, []string{"ip", "jail"}),
|
||||||
|
Replaces: []string{"fail2ban-client set banip"}},
|
||||||
{Name: "unban", Description: "Let one address go, from one jail or from every jail when none is named.",
|
{Name: "unban", Description: "Let one address go, from one jail or from every jail when none is named.",
|
||||||
Input: schema(map[string]string{"ip": "the address", "jail": "one jail (optional)"}, []string{"ip"})},
|
Input: schema(map[string]string{"ip": "the address", "jail": "one jail (optional)"}, []string{"ip"}),
|
||||||
|
Replaces: []string{"fail2ban-client unban", "fail2ban-client set unbanip"}},
|
||||||
}},
|
}},
|
||||||
// The packet filter's verbs (novox/hq ADR 0170): what a person asks a machine's filter whatever
|
// The packet filter's verbs (novox/hq ADR 0170): what a person asks a machine's filter whatever
|
||||||
// filter answers — the rules as enforced, reload the mesh's own, remove one thing the mesh did
|
// filter answers — the rules as enforced, reload the mesh's own, remove one thing the mesh did
|
||||||
@@ -210,15 +213,17 @@ var defaultSeats = append([]Seat{
|
|||||||
"ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or " +
|
"ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or " +
|
||||||
"chain when asked.",
|
"chain when asked.",
|
||||||
Input: schema(map[string]string{"table": "one nftables table, as `family name` (optional)",
|
Input: schema(map[string]string{"table": "one nftables table, as `family name` (optional)",
|
||||||
"chain": "one chain of that table (optional)"}, nil)},
|
"chain": "one chain of that table (optional)"}, nil),
|
||||||
|
Replaces: []string{"nft list ruleset", "iptables -L", "iptables-save"}},
|
||||||
{Name: "reload", Description: "Load the mesh's own filter again from the file the mesh writes, " +
|
{Name: "reload", Description: "Load the mesh's own filter again from the file the mesh writes, " +
|
||||||
"and answer with the mesh's table as loaded.",
|
"and answer with the mesh's table as loaded.",
|
||||||
Input: schema(map[string]string{}, nil)},
|
Input: schema(map[string]string{}, nil), Replaces: []string{"nft -f"}},
|
||||||
{Name: "remove", Description: "Remove one rule set the mesh did not write, named exactly as the " +
|
{Name: "remove", Description: "Remove one rule set the mesh did not write, named exactly as the " +
|
||||||
"host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain " +
|
"host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain " +
|
||||||
"DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an " +
|
"DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an " +
|
||||||
"active found firewall's chains. An operator's act, by name, never a flush.",
|
"active found firewall's chains. An operator's act, by name, never a flush.",
|
||||||
Input: schema(map[string]string{"where": "the rule set, as `node show` lists it"}, []string{"where"})},
|
Input: schema(map[string]string{"where": "the rule set, as `node show` lists it"}, []string{"where"}),
|
||||||
|
Replaces: []string{"nft delete", "iptables -X"}},
|
||||||
}},
|
}},
|
||||||
// The machine's service manager (novox/hq ADR 0177). The host applies every declared unit,
|
// The machine's service manager (novox/hq ADR 0177). The host applies every declared unit,
|
||||||
// system or user scope; the holder answers questions and operator acts about them, each verb
|
// system or user scope; the holder answers questions and operator acts about them, each verb
|
||||||
@@ -349,19 +354,32 @@ func UseSeats(s []Seat) {
|
|||||||
// is never stored (Verb.Optional), and a row is what the working set holds once the store is read: a verb
|
// is never stored (Verb.Optional), and a row is what the working set holds once the store is read: a verb
|
||||||
// the seeding added to the row — `failed`, `checks` — would otherwise come back required, and every holder
|
// the seeding added to the row — `failed`, `checks` — would otherwise come back required, and every holder
|
||||||
// not serving it yet would be refused at registration and at handover, which the mark exists to prevent.
|
// not serving it yet would be refused at registration and at handover, which the mark exists to prevent.
|
||||||
|
//
|
||||||
|
// **And what a verb replaces is the compiled one** (novox/hq ADR 0245): a row seeded before the field
|
||||||
|
// existed carries none, and re-seeding widens a verb's arguments only, so read from the row alone the
|
||||||
|
// `tools` answer — what the mesh MCP server's search and the agent's instructions are built from — would
|
||||||
|
// say no verb replaces anything on a mesh whose rows exist. The mesh's statement of what a role replaces
|
||||||
|
// is this binary's, like the optional mark.
|
||||||
func optionalAsCompiled(row, compiled []Verb) []Verb {
|
func optionalAsCompiled(row, compiled []Verb) []Verb {
|
||||||
optional := map[string]bool{}
|
optional := map[string]bool{}
|
||||||
|
replaces := map[string][]string{}
|
||||||
for _, v := range compiled {
|
for _, v := range compiled {
|
||||||
if v.Optional {
|
if v.Optional {
|
||||||
optional[v.Name] = true
|
optional[v.Name] = true
|
||||||
}
|
}
|
||||||
|
if len(v.Replaces) > 0 {
|
||||||
|
replaces[v.Name] = v.Replaces
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(optional) == 0 {
|
if len(optional) == 0 && len(replaces) == 0 {
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
out := make([]Verb, len(row))
|
out := make([]Verb, len(row))
|
||||||
for i, v := range row {
|
for i, v := range row {
|
||||||
v.Optional = optional[v.Name]
|
v.Optional = optional[v.Name]
|
||||||
|
if r, ok := replaces[v.Name]; ok {
|
||||||
|
v.Replaces = r
|
||||||
|
}
|
||||||
out[i] = v
|
out[i] = v
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -597,19 +615,19 @@ func serviceManagerVerbs() []Verb {
|
|||||||
unit := map[string]string{"unit": "the unit's name, as the service manager knows it"}
|
unit := map[string]string{"unit": "the unit's name, as the service manager knows it"}
|
||||||
return []Verb{
|
return []Verb{
|
||||||
{Name: "units", Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.",
|
{Name: "units", Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.",
|
||||||
Input: scoped(map[string]string{"pattern": "a glob the unit's name must match (optional)"}, nil)},
|
Input: scoped(map[string]string{"pattern": "a glob the unit's name must match (optional)"}, nil), Replaces: []string{"systemctl list-units"}},
|
||||||
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.",
|
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.",
|
||||||
Input: scoped(unit, []string{"unit"})},
|
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl status", "systemctl is-active"}},
|
||||||
{Name: "start", Description: "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.",
|
{Name: "start", Description: "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.",
|
||||||
Input: scoped(unit, []string{"unit"})},
|
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl start"}},
|
||||||
{Name: "stop", Description: "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.",
|
{Name: "stop", Description: "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.",
|
||||||
Input: scoped(unit, []string{"unit"})},
|
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl stop"}},
|
||||||
{Name: "restart", Description: "Restart one unit.",
|
{Name: "restart", Description: "Restart one unit.",
|
||||||
Input: scoped(unit, []string{"unit"})},
|
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl restart"}},
|
||||||
{Name: "enable", Description: "Make one unit start at boot (or at the account's login, in user scope).",
|
{Name: "enable", Description: "Make one unit start at boot (or at the account's login, in user scope).",
|
||||||
Input: scoped(unit, []string{"unit"})},
|
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl enable"}},
|
||||||
{Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).",
|
{Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).",
|
||||||
Input: scoped(unit, []string{"unit"})},
|
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl disable"}},
|
||||||
// **A window, not only a tail** (the operator's direction 2026-10-07): an incident is read for the
|
// **A window, not only a tail** (the operator's direction 2026-10-07): an incident is read for the
|
||||||
// minutes it happened in, and with no window on the verb a person reached for a shell. Every
|
// minutes it happened in, and with no window on the verb a person reached for a shell. Every
|
||||||
// argument is the holder's to validate — passed to journalctl as one word of its own, never through
|
// argument is the holder's to validate — passed to journalctl as one word of its own, never through
|
||||||
@@ -624,14 +642,16 @@ func serviceManagerVerbs() []Verb {
|
|||||||
"until": "the window's end, in the same forms (optional; now when absent)",
|
"until": "the window's end, in the same forms (optional; now when absent)",
|
||||||
"match": "only the lines holding this text, as written — a fixed string, not a pattern (optional)",
|
"match": "only the lines holding this text, as written — a fixed string, not a pattern (optional)",
|
||||||
"priority": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)",
|
"priority": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)",
|
||||||
}, []string{"unit"})},
|
}, []string{"unit"}),
|
||||||
|
Replaces: []string{"journalctl"}},
|
||||||
// What has failed, on the seat rather than as one holder's own tool: whatever holds the role answers
|
// What has failed, on the seat rather than as one holder's own tool: whatever holds the role answers
|
||||||
// it, so a caller asks every machine the same way. **Optional while its holders catch up**: the
|
// it, so a caller asks every machine the same way. **Optional while its holders catch up**: the
|
||||||
// systemd module running today serves it as its own systemd_failed, and a required verb would
|
// systemd module running today serves it as its own systemd_failed, and a required verb would
|
||||||
// refuse it before the version serving `failed` could be delivered.
|
// refuse it before the version serving `failed` could be delivered.
|
||||||
{Name: "failed", Optional: true, Description: "Every failed unit on this machine, in the system manager and in the operator " +
|
{Name: "failed", Optional: true, Description: "Every failed unit on this machine, in the system manager and in the operator " +
|
||||||
"account's; a manager that does not answer is reported with its error, never as nothing failed.",
|
"account's; a manager that does not answer is reported with its error, never as nothing failed.",
|
||||||
Input: schema(map[string]string{"scope": "\"system\" or \"user\": only that manager (both when absent)"}, nil)},
|
Input: schema(map[string]string{"scope": "\"system\" or \"user\": only that manager (both when absent)"}, nil),
|
||||||
|
Replaces: []string{"systemctl --failed"}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,10 +664,10 @@ func backupVerbs() []Verb {
|
|||||||
return []Verb{
|
return []Verb{
|
||||||
{Name: "backed-up", Description: "What this machine backs up: each module, what it declared, " +
|
{Name: "backed-up", Description: "What this machine backs up: each module, what it declared, " +
|
||||||
"its last good night, how many restore points are kept and the repository's size.",
|
"its last good night, how many restore points are kept and the repository's size.",
|
||||||
Input: schema(map[string]string{"module": "one module (optional)"}, nil)},
|
Input: schema(map[string]string{"module": "one module (optional)"}, nil), Replaces: []string{"restic snapshots"}},
|
||||||
{Name: "now", Description: "Take a backup now, of one module or of every module on this " +
|
{Name: "now", Description: "Take a backup now, of one module or of every module on this " +
|
||||||
"machine — before a migration, a retirement or anything else that could go wrong.",
|
"machine — before a migration, a retirement or anything else that could go wrong.",
|
||||||
Input: schema(map[string]string{"module": "one module (optional)"}, nil)},
|
Input: schema(map[string]string{"module": "one module (optional)"}, nil), Replaces: []string{"restic backup"}},
|
||||||
{Name: "restore", Description: "Restore one module's data from a restore point BESIDE the live " +
|
{Name: "restore", Description: "Restore one module's data from a restore point BESIDE the live " +
|
||||||
"data, never over it: each directory as <path>.restored-<date>. Swapping it in is a " +
|
"data, never over it: each directory as <path>.restored-<date>. Swapping it in is a " +
|
||||||
"person's act. Lists the restore points when none is named.",
|
"person's act. Lists the restore points when none is named.",
|
||||||
@@ -655,7 +675,7 @@ func backupVerbs() []Verb {
|
|||||||
"module": "the module",
|
"module": "the module",
|
||||||
"snapshot": "the restore point (from `backed-up`; the newest when omitted)",
|
"snapshot": "the restore point (from `backed-up`; the newest when omitted)",
|
||||||
"path": "one of the module's directories (all of them when omitted)",
|
"path": "one of the module's directories (all of them when omitted)",
|
||||||
}, []string{"module"})},
|
}, []string{"module"}), Replaces: []string{"restic restore"}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ type Verb struct {
|
|||||||
// the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the
|
// the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the
|
||||||
// seat set's protocol is the compiled one.
|
// seat set's protocol is the compiled one.
|
||||||
Optional bool `json:"-"`
|
Optional bool `json:"-"`
|
||||||
|
// Replaces are the shell commands this verb is the mesh's way to do, each as it is typed —
|
||||||
|
// `journalctl`, `systemctl restart` — so the agent finds the verb by the command it would have run
|
||||||
|
// and is told to call it instead (novox/hq ADR 0245): the mesh MCP server's search matches them, the agent's
|
||||||
|
// instructions list them, and the guard on its shell names the verb when it refuses a work-around.
|
||||||
|
// A command line is matched by its words in order, the first being the command's name; `edit
|
||||||
|
// /etc/hosts` and `HOSTALIASES` name the guard's two local work-arounds for a mesh name.
|
||||||
|
Replaces []string `json:"replaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Verb) UnmarshalJSON(raw []byte) error {
|
func (v *Verb) UnmarshalJSON(raw []byte) error {
|
||||||
@@ -94,7 +101,8 @@ var ControllerVerbs = []Verb{
|
|||||||
{Name: "nodes", Description: "Every machine the mesh knows, with whether it is converged or adopted.",
|
{Name: "nodes", Description: "Every machine the mesh knows, with whether it is converged or adopted.",
|
||||||
Input: schema(nil, nil)},
|
Input: schema(nil, nil)},
|
||||||
{Name: "node", Description: "What one machine reported it can do, what it is assigned, and why.",
|
{Name: "node", Description: "What one machine reported it can do, what it is assigned, and why.",
|
||||||
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
|
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"}),
|
||||||
|
Replaces: []string{"hostnamectl", "uptime"}},
|
||||||
{Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " +
|
{Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " +
|
||||||
"and which machines run it.",
|
"and which machines run it.",
|
||||||
Input: schema(nil, nil)},
|
Input: schema(nil, nil)},
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ type CarriedMove struct {
|
|||||||
To string `json:"to"`
|
To string `json:"to"`
|
||||||
Build string `json:"build,omitempty"`
|
Build string `json:"build,omitempty"`
|
||||||
// Recreates is what the send does to the module's containers, in the mesh's words — how many it
|
// Recreates is what the send does to the module's containers, in the mesh's words — how many it
|
||||||
// recreates, and whether with a new image or only their declaration (novox/hq ADR 0242); empty when
|
// recreates, and whether with a new image or only their declaration (novox/hq ADR 0245); empty when
|
||||||
// it recreates none, or when the build the machine ran is not known.
|
// it recreates none, or when the build the machine ran is not known.
|
||||||
Recreates string `json:"recreates,omitempty"`
|
Recreates string `json:"recreates,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user