diff --git a/node-tools/cmd/node-tools/main.go b/node-tools/cmd/node-tools/main.go index 7c821fc..7da745a 100644 --- a/node-tools/cmd/node-tools/main.go +++ b/node-tools/cmd/node-tools/main.go @@ -50,6 +50,8 @@ func main() { until := time.Now().Add(startWait) cred := credential(until) conn := connectPatiently(cred, until) + // And the seat list it describes the seats by, read through the seat mesh-catalog (novox/hq ADR 0636 §4). + _ = startSeatList(conn) served, err := runtime.ServedModulesFrom(os.Getenv(runtime.ToolModules), cred.Module) if err != nil { diff --git a/node-tools/cmd/node-tools/seatlist.go b/node-tools/cmd/node-tools/seatlist.go new file mode 100644 index 0000000..ad53c10 --- /dev/null +++ b/node-tools/cmd/node-tools/seatlist.go @@ -0,0 +1,53 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatdef" + "git.novox.be/novox/mesh-sdk/go/seatlist" + "github.com/novox/mesh-tools/node-tools/internal/bus" +) + +// The tool runner reads the seat list as every part does (novox/hq ADR 0635 §1, ADR 0636): through the seat mesh-catalog, +// from the bootstrapped definitions until it has been read, and following the seat's events. What it holds is given to +// catalogue at every change; the console describes the seats from it, so the descriptions are the mesh's own and not a +// list compiled into the runner. + +// busOfTheRunner is the reader's bus over the runner's connection: a verb of the seat asked on its subject, an event heard +// on the seat's event subject. +type busOfTheRunner struct{ conn *bus.Conn } + +func (b busOfTheRunner) Ask(key string, body any) (json.RawMessage, error) { + seat, verb, _ := strings.Cut(strings.TrimPrefix(key, "seat:"), ".") + answered, err := b.conn.Ask(key, body, "mesh.seat."+seat+".tool."+verb) + if err != nil { + return nil, err + } + return answered.Result, nil +} + +func (b busOfTheRunner) Subscribe(pattern string, handle func(string, json.RawMessage) error) error { + seat, event, _ := strings.Cut(pattern, ".") + _, err := b.conn.Raw(seatdef.EventSubject(seat, event), func(_ string, data []byte) []byte { + _ = handle(pattern, data) + return nil // an event is answered by nobody + }) + return err +} + +// startSeatList starts the runner's reader and gives catalogue its bootstrapped seats at once. +func startSeatList(conn *bus.Conn) *seatlist.Reader { + reader, err := seatlist.Start(seatlist.Config{Bus: busOfTheRunner{conn}, Bootstrap: seatlist.Bootstrapped(), + OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }}) + if err != nil { + log.Printf("node-tools: %v", fmt.Errorf("the tool runner cannot follow the seat list: %w", err)) + } + if reader != nil { + catalogue.UseSeats(reader.Seats()) + } + return reader +} diff --git a/node-tools/go.mod b/node-tools/go.mod index 1138870..3d1ed80 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -3,6 +3,8 @@ module github.com/novox/mesh-tools/node-tools go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000 github.com/nats-io/nats.go v1.54.0 golang.org/x/sys v0.48.0 ) diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index f4604b5..64c21ec 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -321,7 +321,7 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ // What should have answered: every assignment of a module that declares tools. Silence is named; // a module with no tools is never a name here. - x.Replaces = replacesOf(catalogue.DefaultSeats(), recorded) + x.Replaces = replacesOf(catalogue.Seats(), recorded) for _, m := range recorded { if !m.Tools { continue diff --git a/node-tools/internal/console/manifest_waits_test.go b/node-tools/internal/console/manifest_waits_test.go index d651d68..150389a 100644 --- a/node-tools/internal/console/manifest_waits_test.go +++ b/node-tools/internal/console/manifest_waits_test.go @@ -36,8 +36,8 @@ func TestTheManifestNamesTheFirstFamiliesOneByOne(t *testing.T) { } } // A wait is no consumer (ADR 0367 §2): what it consumes is the four views families of its first views (ADR 0615 §5, issue - // 1966 TLS-5), and nothing else. - wantConsumes := []string{"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.seat-holders", + // 1966 TLS-5) and the two events of the seat that serves the seat list (ADR 0636 §4), and nothing else. + wantConsumes := []string{"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.seat-holders", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules"} if !reflect.DeepEqual(m.Consumes, wantConsumes) { t.Errorf("the tool runner consumes %v, want %v", m.Consumes, wantConsumes) diff --git a/node-tools/internal/console/mcp.go b/node-tools/internal/console/mcp.go index 70490d6..ee93c89 100644 --- a/node-tools/internal/console/mcp.go +++ b/node-tools/internal/console/mcp.go @@ -75,7 +75,7 @@ func (s *Surface) seatsAnswer() map[string]any { placed, err := seatHoldersOn(s.src) x := &index{Placed: placed} seats := []map[string]any{} - for _, st := range catalogue.DefaultSeats() { + for _, st := range catalogue.Seats() { holders := x.placedHolders(st.Name, "") if holders == nil { holders = []delivery.SeatHolder{} diff --git a/node-tools/internal/console/search_test.go b/node-tools/internal/console/search_test.go index c624a07..821072f 100644 --- a/node-tools/internal/console/search_test.go +++ b/node-tools/internal/console/search_test.go @@ -46,7 +46,7 @@ func meshToSearch(t *testing.T) *index { Description: does, Subject: "mesh.mod.docker.tool." + tool + ".anchor"}) } x, _, _ := indexOfEndpoints(endpoints) - x.Replaces = replacesOf(catalogue.DefaultSeats(), []recordedModule{{Module: "docker", On: []string{"anchor"}, Tools: true, + x.Replaces = replacesOf(catalogue.Seats(), []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 } diff --git a/node-tools/internal/console/seats_for_tests_test.go b/node-tools/internal/console/seats_for_tests_test.go new file mode 100644 index 0000000..834bc56 --- /dev/null +++ b/node-tools/internal/console/seats_for_tests_test.go @@ -0,0 +1,10 @@ +package console + +import ( + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest" +) + +// The seats the tests describe are given to catalogue as the tool runner gives the ones it read from the bus +// (novox/hq ADR 0635 §1); catalogue compiles none. +func init() { catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions())) } diff --git a/node-tools/internal/firstview/firstview.go b/node-tools/internal/firstview/firstview.go index c4b23c9..e672aa3 100644 --- a/node-tools/internal/firstview/firstview.go +++ b/node-tools/internal/firstview/firstview.go @@ -11,6 +11,8 @@ import ( "context" "encoding/json" "fmt" + conditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" + condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" "sort" "sync" "time" @@ -135,7 +137,7 @@ func (v *Views) hear(env bus.Envelope) error { // finding writes or removes the runner's finding of a first view unanswered past its bound, in its own state. func (v *Views) finding(st views.Status, raised bool) { - key, value, err := views.UnansweredFinding(v.deps.Module, st, time.Now()) + key, value, err := conditionOf(views.UnansweredFinding(v.deps.Module, st, time.Now())) if err != nil { v.logf("[mesh-tools] the finding of the first view %s is not made: %v", st.Tool, err) return @@ -198,3 +200,21 @@ func (v *Views) Again(ctx context.Context, why string) []error { } return errs } + +// conditionOf is a finding in the layout of the seat mesh-conditions' state findings (its contract, ADR 0634 §1), about a +// module, a warning. +func conditionOf(f views.Finding) (string, []byte, error) { + key := conditions.Key(scopeModule, f.ID, f.Kind) + v := condseat.FindingsByKeyValue{V: 1, Key: key, Kind: f.Kind, Observer: f.Observer, Severity: conditions.Warning, + Subject: condseat.Subject{Scope: scopeModule, ID: f.ID}, Summary: f.Summary, Headline: f.Headline, + Explanation: f.Explanation, Source: f.Observer + "." + f.Kind, At: f.At.UTC().Format(time.RFC3339), + Evidence: []condseat.Evidence{{At: f.At.UTC().Format(time.RFC3339), Said: f.Summary}}} + if err := conditions.CheckFinding(v); err != nil { + return "", nil, err + } + raw, err := json.Marshal(v) + return key, raw, err +} + +// scopeModule is the scope of a finding about a module. +const scopeModule = "module" diff --git a/node-tools/module.json b/node-tools/module.json index f3bec56..08b4f05 100644 --- a/node-tools/module.json +++ b/node-tools/module.json @@ -71,6 +71,8 @@ "findings" ], "consumes": [ + "mesh-catalog.seat-definition-changed", + "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.seat-holders", "mesh-delivery-indexer.views.manifests",