Files
mesh-catalog/modules/mesh-provisioner/cmd/mesh-provisioner/holdertools.go
T
jschoubben 6d67c22bf3 One bad module must not drop its node: the provisioner composes each module's plan on its own (issue 774)
novox since 15:36 (nats's built manifest) and ace since 15:37 (baserow's config.json) had no plans at all, so the
vault refused every secret their clients asked. The holder now composes with frombus.PlansHeld: a module whose build
or plan refuses is held, its why kept in the new state refused and read by provisioner_plans, its last plan and its
provisions left as they were, and every other module on the node composed and written. Pins mesh-sdk 59311407.
2026-10-30 17:55:19 +01:00

125 lines
4.5 KiB
Go

package main
// The holder's tools (novox/hq ADR 0570 Consequences, ADR 0564 §12's rule): a read of every key of `provisions` and
// `plans`, one key whole, each state's fill, composing one node now, and reading the operator's answers now.
import (
"context"
"encoding/json"
"fmt"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// The holder's tools.
const (
ProvisionsTool = "provisioner_provisions"
PlansTool = "provisioner_plans"
ComposeTool = "provisioner_compose"
DecideTool = "provisioner_decide"
)
func str(d string) map[string]any { return map[string]any{"type": "string", "description": d} }
func holderTools(h *Holder) []stdio.Tool {
arg := func(a map[string]any, k string) string { s, _ := a[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: ProvisionsTool,
Description: "Every provision the seat keeps (novox/hq ADR 0570): its key <node>.<consumer>.<provision>[.<local>], " +
"provider, consumer and state (asked, provided, waits, failed, retiring, with why), narrowed by node and " +
"state, with how many keys and bytes the state holds; or one key whole.",
Input: map[string]any{"type": "object", "properties": map[string]any{"key": str("one key, whole"),
"node": str("a consumer's node"), "state": str("asked, provided, waits, failed or retiring")}},
Run: func(a map[string]any) (any, error) {
return h.read(ProvisionsState, arg(a, "key"), arg(a, "node"), arg(a, "state"))
}},
{Name: PlansTool,
Description: "Every module's provisioning plan the seat composed, <node>.<module>, narrowed by node, with how many " +
"keys and bytes the state holds, and each placed module held with no plan composed and why (refused: its " +
"manifest or its plan refuses, the node's other modules composed); or one plan whole.",
Input: map[string]any{"type": "object", "properties": map[string]any{"key": str("one plan, <node>.<module>"),
"node": str("a node")}},
Run: func(a map[string]any) (any, error) {
out, err := h.read(PlansState, arg(a, "key"), arg(a, "node"), "")
if err != nil || arg(a, "key") != "" {
return out, err
}
refused, err := h.read(RefusedState, "", arg(a, "node"), "")
if err != nil {
return nil, err
}
out.(map[string]any)[RefusedState] = refused.(map[string]any)[RefusedState]
return out, nil
}},
{Name: ComposeTool,
Description: "Compose one node's plans and provisions now from the bus, as a change event would, and say what " +
"was written, removed, changed and asked.",
Input: map[string]any{"type": "object", "properties": map[string]any{"node": str("the node")},
"required": []string{"node"}},
Run: func(a map[string]any) (any, error) {
if arg(a, "node") == "" {
return nil, fmt.Errorf("compose names the node")
}
return h.ComposeNode(context.Background(), arg(a, "node"))
}},
{Name: DecideTool,
Description: "Read the operator's answers to every retiring provision's ask now, writing each warrant into its " +
"key, as the router's word on an ask would.",
Run: func(map[string]any) (any, error) { return map[string]any{"said": h.Decide()}, nil }},
}
}
// read is a state's keys narrowed, or one whole, with the state's fill.
func (h *Holder) read(state, key, node, want string) (any, error) {
if key != "" {
e, err := h.Store.Get(state, key)
if err != nil {
return nil, err
}
if e == nil {
return nil, fmt.Errorf("%s holds no %s", state, key)
}
return json.RawMessage(e.Value), nil
}
keys, err := h.Store.Keys(state)
if err != nil {
return nil, err
}
rows := []map[string]any{}
bytes := 0
for _, k := range keys {
e, err := h.Store.Get(state, k)
if err != nil || e == nil {
continue
}
bytes += len(e.Value)
if node != "" && !strings.HasPrefix(k, node+".") {
continue
}
row := map[string]any{"key": k}
if state == ProvisionsState {
p, err := provisions.Decode(e.Value)
if err != nil {
row["unreadable"] = err.Error()
} else {
if want != "" && p.State != want {
continue
}
row["state"], row["provider"], row["why"] = p.State, p.Provider.Module+" on "+p.Provider.Node, p.Why
}
}
if state == RefusedState {
var r Refusal
if err := json.Unmarshal(e.Value, &r); err != nil {
row["unreadable"] = err.Error()
} else {
row["module"], row["why"], row["since"] = r.Module, r.Why, r.Since
}
}
rows = append(rows, row)
}
return map[string]any{state: rows, "count": len(rows), "fill": map[string]int{"keys": len(keys), "bytes": bytes}}, nil
}