302 lines
19 KiB
Go
302 lines
19 KiB
Go
// Command mesh-delivery-view-tools serves the read tools of the indexer's modules (novox/hq issue 1562): the runtime launches it for a
|
|
// call, it reads the indexer's modules bucket through the view's reads (mesh-delivery-indexer.modules) and writes nothing.
|
|
// The view is the other program: the indexer serves no tool (ADR 0437 §4).
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"mesh-delivery-view/internal/view"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Tools are the read tools over a modules state, and build_usage over the build modules' states.
|
|
func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{Name: view.StatusTool,
|
|
Description: "The mesh's status, derived by the view from the buckets it reads (novox/hq ADR 0564 §10), nothing written as a " +
|
|
"summary: every open condition of the condition keeper (mesh-conditions), urgent first then oldest; what is quiet (each " +
|
|
"node the watchman says is offline, each key it says stopped moving); each module whose want and have differ on a node " +
|
|
"(moving, or stuck with the have's or the watchman's words), as mesh-sdk's delivery motion derives it; and what " +
|
|
"mesh-healers did in the last seven days, by outcome. A part that cannot be read says so, never as none; well is true " +
|
|
"only when nothing is open, quiet or stuck and every part was read. Read-only.",
|
|
Run: func(map[string]any) (any, error) { return r.Status(time.Now()), nil }},
|
|
{Name: view.ReachReadTool,
|
|
Description: "Whether a merge is on the machines: per repository, the reached head (the newest merge every node runs a build " +
|
|
"containing, as the view derives it from the indexer's modules, the wants and the haves; a merge at or before it is on the " +
|
|
"machines), what holds the next one (a module on a node, the indexer's lag), and per module its fingerprint, the nodes " +
|
|
"that want it, its changes (fingerprint, the merge it first appeared at, its source revision), its floor and the nodes " +
|
|
"where it is not settled or not healthy (novox/hq ADR 0556). Give a repository (owner/name) for one, none for every one. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "the repository, owner/name; none for every repository"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
repository, _ := args["repository"].(string)
|
|
return r.Reach(repository)
|
|
}},
|
|
{Name: view.ModuleHistoryTool,
|
|
Description: "The history of one module's value in the indexer's modules state (modules.<module>), with times: each value it " +
|
|
"still keeps (up to 20), newest first, with its revision, when it was written, put or del, the repository and commit " +
|
|
"it was computed at, its source revision (src), its new-form fingerprint (fp2) or why it has none (none2), its manifest " +
|
|
"digest and path. Shows when a merge wrote a module, and when an older merge wrote it back. Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "the module's name"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
name, _ := args["module"].(string)
|
|
return r.ModuleHistory(name)
|
|
}},
|
|
{Name: view.HeardTool,
|
|
Description: "The forge stream's (FORGE, the forge bridge's) recent messages as the indexer heard them: for each merged, pull, " +
|
|
"closed or open message it handled, newest first (its last 64), the subject, the stream sequence, how often the server " +
|
|
"delivered it (a redelivery shows above 1), when it was published and handled, its kind, repository, pull request, " +
|
|
"branch and commit (the merge commit, or the head), and what the indexer did: applied (with the modules written, or, " +
|
|
"for a late merged message, the later merge already taken in, when nothing was written), left for redelivery or dropped, with why. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"limit": map[string]any{"type": "integer", "description": "at most this many messages, newest first (default every one kept)"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
limit, _ := args["limit"].(float64)
|
|
return r.Heard(int(limit))
|
|
}},
|
|
{Name: view.BuildUsageTool,
|
|
Description: "How the build nodes are used, and whether a node is eligible to build, per node (every node holding a build module or named by a capacity): each build seat it holds (node-build-go, -node, -image) with its capacity " +
|
|
"(mesh-config's resolved value, or the modules' default) and pause, and whether it is eligible to build and why not; the asks " +
|
|
"running now by kind (delivery, check, recheck, try), the keys left running (past the step bound, or their module not running) and the ones " +
|
|
"their module ended for that (a restart, or past the step bound), counted apart and in no median or occupancy; " +
|
|
"the steps started and finished in the last hour and the last day, failed ones, total and median time, by kind, and how long and " +
|
|
"what share of the window the node ran its capacity of asks. Mesh-wide: the backlog of asks waiting and held on each build seat's " +
|
|
"stream, and the tries running. Read from the bus's states (have, mesh-config, the build modules' steps and pauses, the checker's tries); " +
|
|
"the backlog from the view program's reading of the streams; CPU is not on the bus and the answer names the per-node tool. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
|
Run: func(map[string]any) (any, error) { return u.Usage(), nil }},
|
|
{Name: view.ModuleReadTool,
|
|
Description: "What the indexer stored for one module under modules.<module>, as stored: its fingerprint (fp, none), the " +
|
|
"new-form fingerprint (fp2) or why it has none (none2), the inputs it was computed from (inputs2), the module's " +
|
|
"directory (path) and the toolchains it was computed with. Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "the module's name"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
name, _ := args["module"].(string)
|
|
return r.Module(name)
|
|
}},
|
|
{Name: view.ModulesReadTool,
|
|
Description: "Every module the indexer holds, each as stored under modules.<module>. With none2 true, only the modules " +
|
|
"that have no new-form fingerprint, each with the reason (none2). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"none2": map[string]any{"type": "boolean", "description": "only the modules without a new-form fingerprint"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
only, _ := args["none2"].(bool)
|
|
mods, err := r.Modules(only)
|
|
return map[string]any{"modules": mods}, err
|
|
}},
|
|
{Name: view.CandidateReadTool,
|
|
Description: "What the indexer stored for one candidate (the open pull requests of a branch) under candidates.<group>, as stored: " +
|
|
"the pull requests and heads, the base, and per pull request the touched modules with their new-form fingerprint (fp2) or why " +
|
|
"they have none (none2). Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"group"}, "properties": map[string]any{
|
|
"group": map[string]any{"type": "string", "description": "the candidate's group, the branch name its pull requests share"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
group, _ := args["group"].(string)
|
|
return r.Candidate(group)
|
|
}},
|
|
{Name: view.PredefinedReadTool,
|
|
Description: "The catalogue's predefined list as the indexer keeps it (novox/hq ADR 0565 §2): its key list, the list whole as " +
|
|
"predefined.json holds it with the merge it was read at; a merge older than that never replaces it. mesh-config " +
|
|
"publishes the predefined keys from it after its take-over. Read-only.",
|
|
Run: func(map[string]any) (any, error) {
|
|
return view.ControlRecord(r.PredefinedState, delivery.IndexerPredefinedState, delivery.PredefinedListKey)
|
|
}},
|
|
{Name: view.ConfigCandidatesReadTool,
|
|
Description: "Each open pull request's config declarations as the indexer keeps them (novox/hq ADR 0565 §2): per pull " +
|
|
"request (owner.repository.number), its head and per module the config keys its manifests add or change against the " +
|
|
"trunk's, an empty set when none; deleted when it merges or closes. Give key for one. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"key": map[string]any{"type": "string", "description": "one pull request's key, owner.repository.number"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
key, _ := args["key"].(string)
|
|
return view.ControlRecord(r.ConfigCandidatesState, delivery.IndexerConfigCandidatesState, key)
|
|
}},
|
|
{Name: view.CandidatesReadTool,
|
|
Description: "Every candidate the indexer holds, by group, each as stored under candidates.<group>; with module, only those with a " +
|
|
"pull request touching that module. Keys that cannot be read are listed as unreadable. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "only candidates touching this module"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
c, bad, err := r.Candidates(module)
|
|
return map[string]any{"candidates": c, "unreadable": bad}, err
|
|
}},
|
|
{Name: view.BuilderBuildsReadTool,
|
|
Description: "The builder's build keys, each as stored in mesh-delivery-builder_builds: key, module, fingerprint (src2) and build " +
|
|
"(state, by, step with the nodes it was built on, manifest, artifacts with their digests, toolchains, error). Narrow by module " +
|
|
"or state (queued, building, done, failed); at most limit builds (default 50), newest revision first for a module, and the answer says when it was cut and lists keys that are no build key (unreadable). Without a module, give one to get its newest. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "only this module's builds"},
|
|
"state": map[string]any{"type": "string", "description": "only builds in this state"},
|
|
"limit": map[string]any{"type": "integer", "description": "at most this many builds (default 50), newest revision first"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
state, _ := args["state"].(string)
|
|
limit, _ := args["limit"].(float64)
|
|
return r.BuilderBuilds(module, state, int(limit))
|
|
}},
|
|
{Name: view.BuildHistoryTool,
|
|
Description: "Every build that ended since a time (default the last 24 hours), oldest first, from the builder's log build-log (kept 30 " +
|
|
"days): its module, fingerprint, repository, directory, commit and ref, the build node, when it ended, its outcome (done, failed or " +
|
|
"queued), the manifest, the bases it was built against, each artifact made, and seconds, how long it took from its start (absent " +
|
|
"when the start is older than since). With module, that module's builds only; more says the log held more: ask again from the " +
|
|
"last build's time. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "only this module's builds"},
|
|
"since": map[string]any{"type": "string", "description": "RFC 3339 time; default 24 hours ago"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
since := time.Now().Add(-view.DefaultHistorySince)
|
|
if raw, _ := args["since"].(string); strings.TrimSpace(raw) != "" {
|
|
at, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(raw))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("since %q is not an RFC 3339 time: %w", raw, err)
|
|
}
|
|
since = at
|
|
}
|
|
return r.BuildHistory(module, since)
|
|
}},
|
|
{Name: view.BuilderBuildReadTool,
|
|
Description: "One build of the builder as stored. Give the module and its fingerprint (fp2, src2:...), or the module alone for the build " +
|
|
"at the fingerprint the indexer holds for it now, joined through modules.<module>; with commit, only if the indexer holds the module at that commit (a module's value names the commit its fingerprint first appeared at, and stays there through merges that leave the module alone). Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "the module's name"},
|
|
"fp2": map[string]any{"type": "string", "description": "its new-form fingerprint; empty means the indexer's current one"},
|
|
"commit": map[string]any{"type": "string", "description": "the commit the indexer must hold the module at"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
fp2, _ := args["fp2"].(string)
|
|
commit, _ := args["commit"].(string)
|
|
return r.BuilderBuild(module, fp2, commit)
|
|
}},
|
|
{Name: view.WantsReadTool,
|
|
Description: "The assigner's wants and holds as stored in mesh-delivery-assigner_wants: wants by key (want.<node>.<module>) and " +
|
|
"held fingerprints by key (held.<module>.<hex>). Narrow by node and module (a hold has no node and is shown when no node is given); a node and a module read that one key. " +
|
|
"At most 1000 values a call; the answer says when it was cut (cut, note). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Wants(node, module)
|
|
}},
|
|
{Name: view.WantReadTool,
|
|
Description: "One want of the assigner as stored: what the node is to run of the module (fingerprint, the newest it is held back from and the rule, removed, words). Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"node", "module"}, "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Want(node, module)
|
|
}},
|
|
{Name: view.DifferencesReadTool,
|
|
Description: "The assigner's differences between a want and what the controller sends, as stored in mesh-delivery-assigner_differences, " +
|
|
"by key (<node>.<module>), with the rule that explains each or unexplained. Narrow by node and module (both: that one key). " +
|
|
"At most 1000 values a call; the answer says when it was cut (cut, note). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Differences(node, module)
|
|
}},
|
|
{Name: view.HavesReadTool,
|
|
Description: "What each node has, as the node-engine stored it in node-engine_have: by key <node>.<module> (the fingerprint it runs, its phase and health), " +
|
|
"and <node>.node (the node's own facts). Narrow by node and module (both: that one key). " +
|
|
"At most 1000 values a call; the answer says when it was cut (cut, note). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Haves(node, module)
|
|
}},
|
|
{Name: view.TrustsReadTool,
|
|
Description: "What every node trusts a mesh signature from, in one call (novox/hq ADR 0550 §2): each node's record's trusts " +
|
|
"(from node-engine_have <node>.node), whether it names the vault's signing key as mesh-vault publishes it in its " +
|
|
"state signers and whether it still names the controller's, the nodes that do not name the vault's yet (not_yet), " +
|
|
"and every: true once all do, the gate before the controller's lease is handed away. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
|
Run: func(map[string]any) (any, error) {
|
|
return r.Trusts()
|
|
}},
|
|
}
|
|
}
|
|
|
|
var nodeModule = map[string]any{
|
|
"node": map[string]any{"type": "string", "description": "only this node's keys"},
|
|
"module": map[string]any{"type": "string", "description": "only this module's keys"}}
|
|
|
|
func nodeModuleOf(args map[string]any) (string, string) {
|
|
node, _ := args["node"].(string)
|
|
module, _ := args["module"].(string)
|
|
return strings.TrimSpace(node), strings.TrimSpace(module)
|
|
}
|
|
|
|
// socket is the view program's socket, as the program reads it (MESH_DELIVERY_SOCKET, else its default).
|
|
func socket() string {
|
|
if s := strings.TrimSpace(os.Getenv("MESH_DELIVERY_SOCKET")); s != "" {
|
|
return s
|
|
}
|
|
return "/var/lib/mesh-delivery-view-cli/cli.sock"
|
|
}
|
|
|
|
// backlogOf asks the view program on its socket for the backlog it reads from the seats' streams' workers: the tool
|
|
// runtime grants a bundle states, and the stream and worker information is granted to the view program's own account
|
|
// (mesh-sdk buscompose.BuildBacklogReads).
|
|
func backlogOf(path string) func() ([]view.BacklogRow, error) {
|
|
return func() ([]view.BacklogRow, error) {
|
|
s, err := view.Ask(path)
|
|
return s.Backlog, err
|
|
}
|
|
}
|
|
|
|
func read(state stdio.KeptState) view.StateRead {
|
|
return view.StateRead{Keys: state.Keys, History: func(key string) ([]view.Revision, error) {
|
|
revs, err := state.History(key)
|
|
out := make([]view.Revision, 0, len(revs))
|
|
for _, rev := range revs {
|
|
out = append(out, view.Revision{Revision: rev.Revision, At: rev.At, Op: rev.Op, Value: rev.Value})
|
|
}
|
|
return out, err
|
|
}, GetEntry: func(key string) (json.RawMessage, uint64, error) {
|
|
e, err := state.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, 0, err
|
|
}
|
|
return e.Value, e.Revision, nil
|
|
}, Get: func(key string) (json.RawMessage, error) {
|
|
e, err := state.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
return e.Value, nil
|
|
}}
|
|
}
|
|
|
|
func main() {
|
|
r := view.Reader{State: read(stdio.State("mesh-delivery-indexer.modules")), Builds: read(stdio.State("mesh-delivery-builder.builds")), CandidatesState: read(stdio.State("mesh-delivery-indexer.candidates")),
|
|
WantsState: read(stdio.State("mesh-delivery-assigner.wants")), DifferencesState: read(stdio.State("mesh-delivery-assigner.differences")), HavesState: read(stdio.State("node-engine.have")),
|
|
ReachState: read(stdio.State("reach")),
|
|
HeardState: read(stdio.State("mesh-delivery-indexer.heard")),
|
|
SignersState: read(stdio.State("mesh-vault.signers")),
|
|
ConditionsState: read(stdio.State(view.StatusConditionsState)), AttemptsState: read(stdio.State(view.StatusAttemptsState)),
|
|
PresenceState: read(stdio.State(view.StatusPresenceState)), StuckState: read(stdio.State(view.StatusStuckState)),
|
|
PredefinedState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerPredefinedState)),
|
|
ConfigCandidatesState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState)),
|
|
BuildLog: stdio.Log(delivery.BuildLogRead).Since}
|
|
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
|
|
Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())}
|
|
for _, lang := range delivery.AskLanguages {
|
|
module, _ := delivery.BuildModuleOf(lang)
|
|
u.Steps[lang] = read(stdio.State(module + "." + delivery.StepsState))
|
|
u.Pauses[lang] = read(stdio.State(module + "." + delivery.PausesState))
|
|
}
|
|
if err := stdio.Serve("", Tools(r, u)); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|