Given a repository and the files a branch changes (or the modules by name), plans answers with the tiers
the merge handler would produce — the modules those files touch, what packages their source, everything
reachable along the dependency relation — and what each tier does: built and sent to its machines, or
built and left because its policy records. Saved nowhere. On the seat as plans {repository, paths|modules}.
184 lines
9.1 KiB
Go
184 lines
9.1 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// A Verb is one tool a role serves: its name, what it does, and the schema of its arguments and of
|
|
// its answer (novox/hq ADR 0132, design 33 §2).
|
|
//
|
|
// **A name alone is not callable by something that has never seen the mesh before**, which is the
|
|
// whole population a tool surface exists for. So a seat's protocol carries the definition, in the
|
|
// form an agent protocol already uses — a JSON schema for the input — so nothing translates between
|
|
// a seat's idea of an argument and the caller's.
|
|
//
|
|
// A manifest may still write a bare verb name (`"serves": ["price"]`); that is a Verb with only a
|
|
// name, and the module's runtime answers `tools` with the rest. The two forms read into one type so
|
|
// nothing downstream cares which was written.
|
|
type Verb struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Input map[string]any `json:"input,omitempty"`
|
|
Output map[string]any `json:"output,omitempty"`
|
|
}
|
|
|
|
func (v *Verb) UnmarshalJSON(raw []byte) error {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) > 0 && trimmed[0] == '"' {
|
|
var name string
|
|
if err := json.Unmarshal(trimmed, &name); err != nil {
|
|
return err
|
|
}
|
|
*v = Verb{Name: name}
|
|
return nil
|
|
}
|
|
// Strictly, like the manifest around it: a misspelt key in a tool's definition would otherwise
|
|
// describe a tool nobody can call and refuse nothing.
|
|
type plain Verb
|
|
var p plain
|
|
decoder := json.NewDecoder(bytes.NewReader(trimmed))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&p); err != nil {
|
|
return fmt.Errorf("a served verb is a name or {name, description, input, output}: %w", err)
|
|
}
|
|
if p.Name == "" {
|
|
return fmt.Errorf("a served verb has no name: %s", trimmed)
|
|
}
|
|
*v = Verb(p)
|
|
return nil
|
|
}
|
|
|
|
// VerbNames are the names alone, for the grants and the checks that care about nothing else.
|
|
func VerbNames(verbs []Verb) []string {
|
|
out := make([]string, 0, len(verbs))
|
|
for _, v := range verbs {
|
|
out = append(out, v.Name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ControllerSeatName is the seat the control plane holds, whose tools are the mesh's own verbs.
|
|
const ControllerSeatName = "mesh-controller"
|
|
|
|
// ControllerVerbs are the mesh's own verbs, as the `mesh-controller` seat's tools (novox/hq ADR 0154).
|
|
//
|
|
// **The same function the command line calls, and nothing the tool adds** (ADR 0035): each of these
|
|
// is a command the controller's binary already answers, run by the holder of the seat with the
|
|
// arguments below and answered with what the command printed. A verb here is a contract every future
|
|
// holder must implement, which is why the list is short and made of what an operator asks weekly.
|
|
// Additive within a version (design 33 §7); a verb that would break a caller takes a new version.
|
|
var ControllerVerbs = []Verb{
|
|
{Name: "tools", Description: "Every seat's tools, from the mesh's own records: what each role " +
|
|
"answers, whether or not its holder is up. The mesh's own verbs are the mesh-controller seat's.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "status", Description: "What is wrong, what is quiet, what is out of date, and which " +
|
|
"machines are behind what the mesh would send them.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "nodes", Description: "Every machine the mesh knows, with whether it is converged or adopted.",
|
|
Input: schema(nil, nil)},
|
|
{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"})},
|
|
{Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " +
|
|
"and which machines run it.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "seats", Description: "Every seat the mesh defines, what it delivers, and who holds it.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "builds", Description: "What has been built lately and what came of it, for every module or for one; " +
|
|
"or, given a build's id, everything the build machine said while building it, line by line, from the bus.",
|
|
Input: schema(map[string]string{
|
|
"module": "one module's name; every module when absent",
|
|
"log": "a build's id (as `builds` lists it): print what the build machine said, line by line",
|
|
}, nil)},
|
|
{Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " +
|
|
"the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id.",
|
|
Input: schema(map[string]string{
|
|
"id": "a plan's id (as `plans` lists them): that plan, tier by tier",
|
|
"stop": "a plan's id: stop it — what was asked still builds, nothing further is asked",
|
|
"repository": "owner/repository: the plan a merge there would produce, saving nothing (what-if); with paths or modules",
|
|
"paths": "with repository: the files the merge would change, comma-separated, from the repository's root",
|
|
"modules": "with repository: or the modules it would change, comma-separated",
|
|
}, nil)},
|
|
{Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it.",
|
|
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
|
|
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there.",
|
|
Input: schema(map[string]string{"node": "the machine's name", "module": "the module's name"}, []string{"node", "module"})},
|
|
{Name: "unassign", Description: "Take a module off a machine.",
|
|
Input: schema(map[string]string{"node": "the machine's name", "module": "the module's name"}, []string{"node", "module"})},
|
|
{Name: "pin", Description: "Tell a machine which provider answers a provision for it — the module, and the node " +
|
|
"it runs on, both. Asked for when more than one could answer; the refusal lists them.",
|
|
Input: schema(map[string]string{
|
|
"node": "the machine's name",
|
|
"provision": "the provision, as the consumer requires it",
|
|
"from": "the node the chosen provider runs on",
|
|
"module": "the module providing it there",
|
|
}, []string{"node", "provision", "from", "module"})},
|
|
{Name: "unpin", Description: "Take that choice back, putting the question to the mesh again.",
|
|
Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})},
|
|
{Name: "push", Description: "Send a machine everything it should be — or every machine that is behind, when no machine is named.",
|
|
Input: schema(map[string]string{"node": "the machine's name; every machine behind when absent"}, nil)},
|
|
{Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine, " +
|
|
"else every holder): both ends are re-sent together. Or a module's own secret, by machine, module and " +
|
|
"name: made anew and the machine sent, so the module starts again on it — only for a secret its " +
|
|
"definition says it reads at start; a value given to the mesh, or one the module applies to a backend, is refused with the reason.",
|
|
Input: schema(map[string]string{
|
|
"provision": "a pair credential: the provision whose credential to replace",
|
|
"consumer": "with provision: only the holder on this machine (optional)",
|
|
"node": "an own secret: the machine",
|
|
"module": "an own secret: the module",
|
|
"secret": "an own secret: its name in the module's definition",
|
|
}, nil)},
|
|
{Name: "build", Description: "Have the build machine build a repository. Answers at once with the build's id: " +
|
|
"`builds` with that id follows it line by line, and the module is registered when the outcome comes.",
|
|
Input: schema(map[string]string{
|
|
"repository": "the repository's URL, or its path on the forge holding the git seat (owner/name)",
|
|
"path": "the module's directory inside it (optional)",
|
|
"ref": "the branch, tag or commit to build (optional)",
|
|
}, []string{"repository"})},
|
|
}
|
|
|
|
// schema is a JSON schema for an object of string properties, which is every argument the verbs
|
|
// above take. Kept small on purpose: a schema an agent cannot read is a tool it cannot call.
|
|
func schema(properties map[string]string, required []string) map[string]any {
|
|
props := map[string]any{}
|
|
for name, description := range properties {
|
|
props[name] = map[string]any{"type": "string", "description": description}
|
|
}
|
|
out := map[string]any{"type": "object", "properties": props}
|
|
if len(required) > 0 {
|
|
out["required"] = required
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unpromised is what a claim says it serves and the seat's protocol never promised.
|
|
func unpromised(serves []string, promised []Verb) []string {
|
|
has := map[string]bool{}
|
|
for _, v := range promised {
|
|
has[v.Name] = true
|
|
}
|
|
var extra []string
|
|
for _, s := range serves {
|
|
if !has[s] {
|
|
extra = append(extra, s)
|
|
}
|
|
}
|
|
return extra
|
|
}
|
|
|
|
// unservedVerbs is what a seat promises and a claimant's offer for it does not answer.
|
|
func unservedVerbs(tools []string, promised []Verb) []string {
|
|
has := map[string]bool{}
|
|
for _, t := range tools {
|
|
has[t] = true
|
|
}
|
|
var missing []string
|
|
for _, v := range promised {
|
|
if !has[v.Name] {
|
|
missing = append(missing, v.Name)
|
|
}
|
|
}
|
|
return missing
|
|
}
|