package main // Every command line nox takes, and the seat verb or module tool that owns its job now that the controller is // unassigned (novox/hq issue 1126; the map is hq's .work/1126/nox-map.md). A line no seat or module owns says so and // names what to use instead: it never fails silently, and it never pretends to have run. import ( "encoding/json" "fmt" "io" "sort" "strconv" "strings" "time" ) // line is a command line's words after the command's own: positionals and --flags, in any order. type line struct { pos []string flags map[string]string } // boolFlags are the flags that take no value. var boolFlags = map[string]bool{"json": true, "confirm": true, "collected": true, "brief": true, "all": true, "reasoning": true, "clear": true} func parseLine(args []string) (line, error) { l := line{flags: map[string]string{}} for i := 0; i < len(args); i++ { a := args[i] if !strings.HasPrefix(a, "--") || a == "--" { l.pos = append(l.pos, a) continue } name, value, has := strings.Cut(strings.TrimPrefix(a, "--"), "=") if boolFlags[name] { if has { return l, usageErr("--%s takes no value", name) } l.flags[name] = "true" continue } if !has { if i+1 >= len(args) { return l, usageErr("--%s needs a value", name) } i++ value = args[i] } l.flags[name] = value } return l, nil } func (l line) has(flag string) bool { return l.flags[flag] != "" } // usageError is a line nox cannot read: nothing was called. type usageError struct{ msg string } func (u usageError) Error() string { return u.msg } func usageErr(format string, a ...any) error { return usageError{fmt.Sprintf(format, a...)} } // call is what one line asks: an address and its arguments. For a sensitive vault verb, its argument `act` holds the // *handAct that warrants it until run records the act and puts its id there. type call struct { Address string Args map[string]any } // handAct is the act nox records through mesh-hand-acts before a sensitive vault verb, and names to it as `act`: each such // verb judges its own caller, and nox reaches the vault as its machine's runtime, never as a person (novox/hq ADR 0590 // §8). Its verb is the vault verb's address; its what names what the verb acts on, several names space-separated. type handAct struct { Verb, What, Why string } // warranted is c with the hand act that warrants it, for the vault verb at its address. func warranted(c call, what string) call { why, _ := c.Args["why"].(string) if strings.TrimSpace(why) == "" { why = "asked at the operator's terminal" } if c.Args == nil { c.Args = map[string]any{} } c.Args["act"] = &handAct{Verb: c.Address, What: what, Why: why} return c } // command is one of nox's lines. type command struct { words []string usage string does string // build reads the line and answers the call; nil for a line no seat owns. build func(l line) (call, error) // allowed are the flags the line reads; any other is refused before anything is called. allowed []string // instead says, for a line with no owner, what to use; empty for one that is owned. instead string // render shapes a successful answer for reading; nil prints it as the console gave it. render func(text string) string } // gone is the reason every unowned line gives. const gone = "this was the controller's, and the controller is unassigned (novox/hq issue 1126). Nothing was called" // commands is every line, longest words first within a word. var commands = []command{ // What runs where. {words: []string{"status"}, does: "what is wrong, what is quiet, what is moving (the delivery view)", build: fixed("mesh-delivery-view.view_status")}, {words: []string{"nodes"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"), render: renderNodes}, {words: []string{"node", "list"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"), render: renderNodes}, {words: []string{"node", "show"}, usage: "", does: "one node's placement: what it runs and its decided fields", build: nodeArg("mesh-placement.placement")}, {words: []string{"node", "public-domain"}, usage: " [ | --clear] [--why ]", does: "the domain a node composes its routed names under; with a domain or --clear, proposed to placement", allowed: []string{"clear", "why"}, build: publicDomain}, {words: []string{"node", "add"}, instead: "a new machine joins through the seat mesh-enrolment: nox enrol issue --why "}, {words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-placement.placement shows what a node decides"}, {words: []string{"plan"}, usage: "", does: "what the assigner wants the node to run", build: optNode("mesh-delivery-view.assigner_wants")}, {words: []string{"haves"}, usage: "[]", does: "what each node runs, as its node-engine said", build: optNode("mesh-delivery-view.node_haves")}, {words: []string{"switch"}, does: "what each node runs, as its node-engine said", build: fixed("mesh-delivery-view.node_haves")}, {words: []string{"composition"}, usage: " ", does: "one module's composition on a node beside what runs", build: func(l line) (call, error) { if len(l.pos) != 2 { return call{}, usageErr("give the node and the module") } return call{l.pos[0] + "/node-engine.composition", map[string]any{"module": l.pos[1]}}, nil }}, {words: []string{"seats"}, does: "who holds each seat, and where", build: func(line) (call, error) { return call{"mesh-placement.placement", map[string]any{"seats": "true"}}, nil }}, {words: []string{"seat"}, instead: "a seat is held by placing its module: nox assign --why (mesh-placement.propose)"}, {words: []string{"assign"}, usage: " ... --why ", does: "propose placing modules on a node", allowed: []string{"why"}, build: placeModules("place")}, {words: []string{"unassign"}, usage: " ... --why ", does: "propose taking modules off a node", allowed: []string{"why"}, build: placeModules("take-off")}, {words: []string{"pin"}, usage: " --why ", does: "propose which provider a node gets a provision from", allowed: []string{"why"}, build: func(l line) (call, error) { if len(l.pos) != 4 { return call{}, usageErr("give the node, the provision, the provider's node and its module") } return propose(l, map[string]any{"op": "pin", "node": l.pos[0], "provision": l.pos[1], "provider": map[string]any{"node": l.pos[2], "module": l.pos[3]}}) }}, {words: []string{"unpin"}, usage: " --why ", does: "propose putting that question back", allowed: []string{"why"}, build: func(l line) (call, error) { if len(l.pos) != 2 { return call{}, usageErr("give the node and the provision") } return propose(l, map[string]any{"op": "unpin", "node": l.pos[0], "provision": l.pos[1]}) }}, {words: []string{"proposals"}, usage: "[]", does: "the open proposals of placement", build: optArg("mesh-placement.proposals", "id")}, {words: []string{"overlay", "show"}, does: "every node's placement, with its tunnel", build: fixed("mesh-placement.placement")}, {words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its placement: mesh-placement.propose with op decide"}, // Deliveries and builds. {words: []string{"deliveries"}, usage: "[--state s] [--repository owner/repo] [--group g] [--all]", does: "every delivery not final, and those that ended in the last day", allowed: []string{"state", "repository", "group", "all"}, build: func(l line) (call, error) { return call{"mesh-delivery.deliveries", pick(l, "state", "repository", "group", "all")}, nil }}, {words: []string{"delivery"}, usage: "", does: "one delivery or group whole", build: needArg("mesh-delivery.show", "id")}, {words: []string{"groups"}, usage: "[--all]", does: "every delivery group", allowed: []string{"all"}, build: func(l line) (call, error) { return call{"mesh-delivery.groups", pick(l, "all")}, nil }}, {words: []string{"stalled"}, does: "every delivery held past its state's bound", build: fixed("mesh-delivery.stalled")}, {words: []string{"checks"}, usage: " [--number n | --commit c]", does: "what the mesh's checks said of a head", allowed: []string{"number", "commit"}, build: func(l line) (call, error) { if len(l.pos) != 1 { return call{}, usageErr("give the repository, owner/repo") } a := pick(l, "number", "commit") a["repository"] = l.pos[0] return call{"mesh-delivery.checks", a}, nil }}, {words: []string{"try"}, usage: " [--level l] [--trunk t] [--tests t]", does: "try a pushed head before its pull request is opened", allowed: []string{"level", "trunk", "tests"}, build: func(l line) (call, error) { if len(l.pos) != 2 { return call{}, usageErr("give the repository and the head") } a := pick(l, "level", "trunk", "tests") a["repository"], a["head"] = l.pos[0], l.pos[1] return call{"mesh-delivery-checker.try", a}, nil }}, {words: []string{"builds"}, usage: "[--since ]", does: "every build that ended since (the last day by default)", allowed: []string{"since"}, build: func(l line) (call, error) { since := l.flags["since"] if since == "" { since = now().Add(-24 * time.Hour).UTC().Format(time.RFC3339) } return call{"mesh-delivery-builder.builder_builds_since", map[string]any{"since": since}}, nil }}, {words: []string{"upgrade"}, usage: "[] [--policy roll|together|record|default --why ]", does: "whether each module's new builds wait for a person; with --policy, a person's choice", allowed: []string{"policy", "why"}, build: func(l line) (call, error) { if len(l.pos) > 1 { return call{}, usageErr("give at most one module; rolling out is the delivery's: nox deliveries") } a := pick(l, "policy", "why") if len(l.pos) == 1 { a["module"] = l.pos[0] } if a["policy"] != nil && (a["module"] == nil || a["why"] == nil) { return call{}, usageErr("a policy needs the module and --why") } return call{"mesh-wants.upgrade", a}, nil }}, {words: []string{"bus"}, does: "what a bus upgrade would do", build: fixed("mesh-wants.bus")}, // Conditions, the self-check, the healers and hand acts. {words: []string{"conditions", "show"}, usage: "", does: "one condition whole", build: needArg("mesh-conditions.conditions", "key")}, {words: []string{"conditions", "silence"}, usage: " --for <30m|4h|2d> --why [--cause ]", does: "send no message for it a while; a hand act", allowed: []string{"for", "why", "cause"}, build: func(l line) (call, error) { if len(l.pos) != 1 || !l.has("for") || !l.has("why") { return call{}, usageErr("give the key, --for and --why") } a := pick(l, "for", "why", "cause") a["key"] = l.pos[0] return call{"mesh-conditions.silence", a}, nil }}, {words: []string{"conditions", "history"}, usage: "[--days N] [--key K] [--limit N]", does: "every raising, change and clearing lately", allowed: []string{"days", "key", "limit"}, build: func(l line) (call, error) { if l.has("key") { return call{"mesh-conditions.conditions_history", pick(l, "key", "limit")}, nil } a := pick(l, "days") a["history"] = "true" return call{"mesh-conditions.conditions", a}, nil }}, {words: []string{"conditions"}, usage: "[--scope S] [--severity S] [--machine M]", does: "what is wrong now, urgent first", allowed: []string{"scope", "severity", "machine"}, build: func(l line) (call, error) { if len(l.pos) > 0 { return call{}, usageErr("%q is not a conditions line", l.pos[0]) } return call{"mesh-conditions.conditions", pick(l, "scope", "severity", "machine")}, nil }}, {words: []string{"doctor", "probes"}, does: "the self-check's probes", build: fixed("mesh-self-check.probes")}, {words: []string{"doctor", "run"}, does: "run every probe now", build: func(line) (call, error) { return call{"mesh-self-check.doctor", map[string]any{"run": true}}, nil }}, {words: []string{"doctor", "probe"}, usage: "", does: "one probe's verdict whole", build: needArg("mesh-self-check.doctor", "probe")}, {words: []string{"doctor", "signals"}, does: "the watchman's signals and their ages", build: fixed("mesh-watchdogs.signals")}, {words: []string{"doctor"}, does: "the self-check's last verdict", build: fixed("mesh-self-check.doctor")}, {words: []string{"healers"}, usage: "[--days N]", does: "the healers, what they did lately, and their brake", allowed: []string{"days"}, build: func(l line) (call, error) { a, err := numbers(pick(l, "days"), "days") return call{"mesh-healers.healers", a}, err }}, {words: []string{"hand-act", "record"}, usage: " --why [--cause ] [--condition ]", does: "record an act done by hand", allowed: []string{"why", "cause", "condition"}, build: func(l line) (call, error) { if len(l.pos) == 0 || !l.has("why") { return call{}, usageErr("give what was done and --why") } a := pick(l, "why", "cause", "condition") a["what"] = strings.Join(l.pos, " ") return call{"mesh-hand-acts.hand-act", a}, nil }}, {words: []string{"hand-acts"}, usage: "[--days N] [--cause c] [--who w]", does: "what was done by hand lately", allowed: []string{"days", "cause", "who"}, build: func(l line) (call, error) { a, err := numbers(pick(l, "days", "cause", "who"), "days") return call{"mesh-hand-acts.hand-acts", a}, err }}, {words: []string{"stuck"}, does: "what stopped moving, as the watchman holds it", build: fixed("mesh-watchdogs.stuck")}, {words: []string{"dead-letters"}, usage: "[--consumer c]", does: "what a consumer gave up on", allowed: []string{"consumer"}, build: func(l line) (call, error) { return call{"mesh-watchdogs.dead-letters", pick(l, "consumer")}, nil }}, // Config, secrets, the vault, artifacts, licences, modules. {words: []string{"settings", "show"}, usage: " [--node n]", does: "a module's config values", allowed: []string{"node"}, build: func(l line) (call, error) { if len(l.pos) != 1 { return call{}, usageErr("give the module") } a := pick(l, "node") a["action"], a["module"] = "list", l.pos[0] return call{"mesh-config.config", a}, nil }}, {words: []string{"settings", "set"}, usage: " --node n --why ", does: "set one of a module's config values on one node", allowed: []string{"node", "why"}, build: func(l line) (call, error) { return configChange(l, "set") }}, {words: []string{"settings", "clear"}, usage: " --node n --why ", does: "clear one of a module's config values on one node", allowed: []string{"node", "why"}, build: func(l line) (call, error) { return configChange(l, "clear") }}, {words: []string{"config"}, usage: "[list|set|clear|history|propose|proposals] [--module m] [--node n] [--key k] [--value v] [--why w]", does: "the mesh's config values, at any level", allowed: []string{"module", "node", "key", "value", "why", "limit", "proposal", "pull", "clear"}, build: func(l line) (call, error) { if len(l.pos) > 1 { return call{}, usageErr("give at most one action") } a := pick(l, "module", "node", "key", "why", "proposal", "pull", "clear", "limit") if len(l.pos) == 1 { a["action"] = l.pos[0] } if v, ok := l.flags["value"]; ok { a["value"] = jsonOrText(v) } if c, ok := a["clear"]; ok { a["clear"] = c == "true" } a, err := numbers(a, "limit") return call{"mesh-config.config", a}, err }}, {words: []string{"secret", "export"}, does: "every secret the vault holds, as its copy sealed to the operator key", build: func(l line) (call, error) { c, err := fixed("mesh-vault.secret_vault_export")(l) return warranted(c, "the vault's sealed copies"), err }}, {words: []string{"secret", "accept"}, instead: "mesh-vault.accept takes a value from the controller alone, and no seat takes one from a person yet; a module's own secret is asked at a desk (mesh-vault.secret-ask)"}, {words: []string{"secret", "recover"}, instead: "opening a sealed copy locally is not built yet (beside nox enrol open); nox secret export gives the sealed copies"}, {words: []string{"secret"}, instead: "nox secret export; mesh-vault.rotate makes one anew"}, {words: []string{"operator", "key", "show"}, does: "what needs a person's eye in the vault, the operator key among it", build: fixed("mesh-vault.vault_status")}, {words: []string{"operator"}, instead: "no seat sets or makes the operator key yet; nox operator key show reads it (mesh-vault.vault_status)"}, {words: []string{"rotate"}, usage: "[--node n --module m --name s | --provider n --provision p] --why ", does: "make a secret anew", allowed: []string{"node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why"}, build: func(l line) (call, error) { if len(l.pos) == 1 { l.flags["provision"] = l.pos[0] } else if len(l.pos) > 1 { return call{}, usageErr("give the secret by its flags") } if !l.has("why") { return call{}, usageErr("give --why") } what := "provision/" + l.flags["provider"] + "/" + l.flags["provision"] if l.has("node") || l.has("module") || l.has("name") { what = "own/" + l.flags["node"] + "/" + l.flags["module"] + "/" + l.flags["name"] } return warranted(call{"mesh-vault.rotate", pick(l, "node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why")}, what), nil }}, {words: []string{"identity", "show"}, does: "the vault's signing key and authority", build: fixed("mesh-vault.vault_signers")}, {words: []string{"retire", "approve"}, usage: "... --why ", does: "retire the set waiting for a person", allowed: []string{"why"}, build: func(l line) (call, error) { c, err := retirement("mesh-vault.provisioner_retire_approve")(l) return warranted(c, strings.Join(l.pos, " ")), err }}, {words: []string{"retire", "reject"}, usage: "... --why ", does: "keep the set waiting for a person active", allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_reject")}, {words: []string{"retire"}, does: "every retirement waiting for a person", build: fixed("mesh-vault.provisioner_retirement")}, {words: []string{"cleanup", "delete"}, usage: " --why --confirm", does: "delete one retired holder from the ledger", allowed: []string{"why", "confirm"}, build: func(l line) (call, error) { if len(l.pos) != 1 || !l.has("why") { return call{}, usageErr("give the consumer and --why") } a := pick(l, "why") a["consumer"] = l.pos[0] // The vault deletes only when confirm repeats the holder's name (mesh-vault retirement.go); without // --confirm it answers what it would delete. if !l.has("confirm") { // Without --confirm the vault deletes nothing and says so: no act is recorded for it. return call{"mesh-vault.provisioner_delete", a}, nil } a["confirm"] = l.pos[0] return warranted(call{"mesh-vault.provisioner_delete", a}, l.pos[0]), nil }}, {words: []string{"cleanup"}, does: "every retired holder per provider", build: fixed("mesh-vault.provisioner_retirement")}, {words: []string{"artifacts"}, usage: "[--collected]", does: "every artifact the mesh made: kept and why, or eligible", allowed: []string{"collected"}, build: func(l line) (call, error) { return call{"mesh-artifact-sweep.artifacts", pick(l, "collected")}, nil }}, {words: []string{"collection"}, does: "every artifact the mesh made: kept and why, or eligible", build: fixed("mesh-artifact-sweep.artifacts")}, {words: []string{"collect"}, usage: "[--confirm --why ] [--most n]", does: "the sweep: a dry run, or with --confirm the sweep now", allowed: []string{"confirm", "why", "most"}, build: func(l line) (call, error) { if l.has("confirm") && !l.has("why") { return call{}, usageErr("--confirm needs --why") } return call{"mesh-artifact-sweep.collect", pick(l, "confirm", "why", "most")}, nil }}, {words: []string{"images"}, usage: "", does: "every container image a machine's wants and haves name", build: needArg("mesh-artifact-sweep.images", "node")}, {words: []string{"mirrors"}, does: "the bases the mesh copied into its store", build: fixed("mesh-artifact-sweep.mirrors")}, {words: []string{"licence", "list"}, does: "every licence the manager holds", build: fixed("anthropic-licence-manager.licences")}, {words: []string{"licence", "use"}, usage: " ", does: "move a node's agent to another licence", build: func(l line) (call, error) { if len(l.pos) != 2 { return call{}, usageErr("give the consumer (a node) and the licence") } return call{"anthropic-licence-manager.switch", map[string]any{"consumer": l.pos[0], "licence": l.pos[1]}}, nil }}, {words: []string{"licence", "refresh"}, usage: "[]", does: "refresh a licence now, or every due one", build: optArg("anthropic-licence-manager.refresh", "licence")}, {words: []string{"licence", "key"}, does: "the manager's public key", build: fixed("anthropic-licence-manager.public-key")}, {words: []string{"licence"}, instead: "anthropic-licence-manager.adopt takes an API key from a file on the manager's node; nox licence list|use|refresh|key"}, {words: []string{"module", "list"}, does: "every module the indexer holds", build: fixed("mesh-delivery-view.indexer_modules")}, {words: []string{"module", "show"}, usage: "", does: "what the indexer holds of one module", build: needArg("mesh-delivery-view.indexer_module", "module")}, {words: []string{"module", "moved"}, usage: " --why ", does: "take a registered module in now", allowed: []string{"why"}, build: func(l line) (call, error) { if len(l.pos) < 1 { return call{}, usageErr("give the module") } return call{"mesh-delivery-indexer.indexer_take_in", map[string]any{"module": l.pos[0], "why": l.flags["why"]}}, nil }}, {words: []string{"module"}, instead: "the indexer takes modules in from their repositories; nox module list|show|moved, and nothing forgets one by hand yet"}, {words: []string{"register"}, instead: "no seat registers a module from another repository yet (novox/hq ADR 0501 §4); nox module moved takes a registered one in"}, {words: []string{"account", "issue"}, usage: "", does: "mint one bus account the grants part wants, now", build: needArg("mesh-vault.issue-account", "user")}, // Issues and records. {words: []string{"issues"}, usage: "[] [--status s] [--limit n] [--owner o]", does: "the issues, newest first, or those that match", allowed: []string{"status", "limit", "owner", "severity", "needs"}, build: func(l line) (call, error) { a := pick(l, "status", "owner", "severity", "needs", "limit") if len(l.pos) > 0 { a["search"] = strings.Join(l.pos, " ") } a, err := numbers(a, "limit") return call{"issue-tracker.list", a}, err }}, {words: []string{"issue"}, usage: " [--brief]", does: "one issue whole", allowed: []string{"brief"}, build: func(l line) (call, error) { if len(l.pos) != 1 { return call{}, usageErr("give the issue's number") } n, err := strconv.Atoi(l.pos[0]) if err != nil || n < 1 { return call{}, usageErr("%q is not an issue's number", l.pos[0]) } a := map[string]any{"number": n} if l.has("brief") { a["brief"] = true } return call{"issue-tracker.get", a}, nil }}, {words: []string{"kb", "ask"}, usage: "", does: "what holds about something, and why", build: func(l line) (call, error) { if len(l.pos) == 0 { return call{}, usageErr("give the question") } return call{"knowledge-base.ask", map[string]any{"question": strings.Join(l.pos, " ")}}, nil }}, {words: []string{"kb", "get"}, usage: " [--kind research] [--reasoning]", does: "one record by its number", allowed: []string{"kind", "reasoning"}, build: func(l line) (call, error) { if len(l.pos) != 1 { return call{}, usageErr("give the record's number") } n, err := strconv.Atoi(l.pos[0]) if err != nil { return call{}, usageErr("%q is not a record's number", l.pos[0]) } a := pick(l, "kind") a["number"] = n if l.has("reasoning") { a["reasoning"] = true } return call{"knowledge-base.get", a}, nil }}, {words: []string{"progress"}, usage: "[|mesh] [--detail minimal|detailed]", does: "how far the designs have come", allowed: []string{"detail", "since"}, build: func(l line) (call, error) { a := pick(l, "detail", "since") a["of"] = "mesh" if len(l.pos) == 1 { a["of"] = l.pos[0] if _, err := strconv.Atoi(l.pos[0]); err == nil { a["of"] = "design " + l.pos[0] } } return call{"knowledge-base.progress", a}, nil }}, // Enrolment (the seat mesh-enrolment; `enrol open` is local, in main.go). {words: []string{"enrol", "issue"}, usage: " --why [--expires 1h]", does: "a one-time token for a new machine, on the operator's Approve", allowed: []string{"why", "expires"}, build: enrolIssue}, {words: []string{"token", "issue"}, usage: "--new --why ", does: "the same as nox enrol issue", allowed: []string{"new", "node", "why", "expires", "adopted"}, build: func(l line) (call, error) { node := l.flags["new"] if node == "" { node = l.flags["node"] } if node != "" { l.pos = append([]string{node}, l.pos...) } return enrolIssue(l) }}, {words: []string{"enrol", "tokens"}, usage: "[]", does: "every open enrolment, or one", build: optArg("mesh-enrolment.tokens", "node")}, {words: []string{"enrol", "revoke"}, usage: " --why ", does: "end an enrolment at any stage", allowed: []string{"why"}, build: func(l line) (call, error) { if len(l.pos) != 1 || !l.has("why") { return call{}, usageErr("give the node and --why") } return call{"mesh-enrolment.revoke", map[string]any{"node": l.pos[0], "why": l.flags["why"]}}, nil }}, {words: []string{"enrol", "again"}, usage: "", does: "raise an enrolment's ask again", build: needArg("mesh-enrolment.again", "node")}, // Any address. {words: []string{"call"}, usage: "
[]", does: "call any seat verb or module tool, e.g. ace/node-service-manager.status", build: func(l line) (call, error) { if len(l.pos) < 1 || len(l.pos) > 2 { return call{}, usageErr("give the address and at most one JSON object of arguments") } return addressCall(l.pos[0], l.pos[1:]) }}, {words: []string{"ask"}, usage: " []", does: "call one of a module's tools", build: func(l line) (call, error) { if len(l.pos) < 2 || len(l.pos) > 3 { return call{}, usageErr("give the module, the tool and at most one JSON object of arguments") } return addressCall(l.pos[0]+"."+l.pos[1], l.pos[2:]) }}, } // unowned are the controller's own lines that no seat or module took over, and what to use where there is anything. var unowned = map[string]string{ "migrate": "", "prepare": "", "serve": "", "declare": "", "broker": "", "board": "", "api": "", "facts": "", "rehearse": "", "take": "", "converge": "", "adopt": "", "move-placement": "", "move-config": "", "durations": "", "data": "each machine's backups: nox call /node-backup.backed-up", "forget-checks": "nox checks ", "queue": "nox builds; nox stuck", "cancel": "nox deliveries", "clear": "nox deliveries", "kill": "nox deliveries", "replay": "nox dead-letters", "pause": "nox call /node-build-go.pause (or node-build-node, node-build-image)", "resume": "nox call /node-build-go.resume (or node-build-node, node-build-image)", "push": "a merge is delivered by mesh-delivery: nox deliveries; one version on one machine is mesh-delivery-assigner.assigner_put", "deliver": "nox deliveries", "plans": "nox deliveries", "delivery-stop": "nox stalled", "delivery-backups-again": "nox deliveries", "delivery-walks": "nox deliveries", "delivery-order": "nox deliveries", "rollout": "nox upgrade ; delivery is mesh-delivery's: nox deliveries", } // find answers the command whose words begin the line, the longest first, and what follows them. func find(args []string) (*command, []string) { var best *command for i := range commands { c := &commands[i] if len(c.words) > len(args) || (best != nil && len(c.words) <= len(best.words)) { continue } match := true for j, w := range c.words { if args[j] != w { match = false break } } if match { best = c } } if best == nil { return nil, nil } return best, args[len(best.words):] } // read builds the call of a line for a command that has an owner. func (c *command) read(args []string) (call, error) { l, err := parseLine(args) if err != nil { return call{}, err } allowed := map[string]bool{"json": true} for _, f := range c.allowed { allowed[f] = true } for f := range l.flags { if !allowed[f] { return call{}, usageErr("--%s is not a flag of this line", f) } } return c.build(l) } // now is the clock; a variable so a test can say. var now = time.Now func fixed(address string) func(line) (call, error) { return func(l line) (call, error) { if len(l.pos) > 0 { return call{}, usageErr("this line takes no %q", l.pos[0]) } return call{address, map[string]any{}}, nil } } func needArg(address, name string) func(line) (call, error) { return func(l line) (call, error) { if len(l.pos) != 1 { return call{}, usageErr("give the %s", name) } return call{address, map[string]any{name: l.pos[0]}}, nil } } func optArg(address, name string) func(line) (call, error) { return func(l line) (call, error) { switch len(l.pos) { case 0: return call{address, map[string]any{}}, nil case 1: return call{address, map[string]any{name: l.pos[0]}}, nil } return call{}, usageErr("give at most one %s", name) } } func nodeArg(address string) func(line) (call, error) { return needArg(address, "node") } func optNode(address string) func(line) (call, error) { return optArg(address, "node") } // pick is the line's flags named, as string arguments. func pick(l line, names ...string) map[string]any { a := map[string]any{} for _, n := range names { if v, ok := l.flags[n]; ok { a[n] = v } } return a } // numbers turns the named arguments into numbers; the call's address is left to the caller. func numbers(a map[string]any, names ...string) (map[string]any, error) { for _, n := range names { v, ok := a[n].(string) if !ok { continue } i, err := strconv.Atoi(v) if err != nil { return a, usageErr("--%s takes a number, not %q", n, v) } a[n] = i } return a, nil } // propose is a change of placement with its why. func propose(l line, changes ...map[string]any) (call, error) { if !l.has("why") { return call{}, usageErr("a change of placement needs --why; the operator is asked to approve it") } list := make([]any, len(changes)) for i, c := range changes { list[i] = c } return call{"mesh-placement.propose", map[string]any{"why": l.flags["why"], "changes": list}}, nil } func placeModules(op string) func(line) (call, error) { return func(l line) (call, error) { if len(l.pos) < 2 { return call{}, usageErr("give the node and at least one module") } var changes []map[string]any for _, m := range l.pos[1:] { changes = append(changes, map[string]any{"op": op, "node": l.pos[0], "module": m}) } return propose(l, changes...) } } func publicDomain(l line) (call, error) { switch { case len(l.pos) == 1 && !l.has("clear"): return call{"mesh-placement.placement", map[string]any{"node": l.pos[0]}}, nil case len(l.pos) == 1: return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": nil}) case len(l.pos) == 2 && !l.has("clear"): return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": l.pos[1]}) } return call{}, usageErr("give the node, and a domain or --clear") } func configChange(l line, action string) (call, error) { want := 3 if action == "clear" { want = 2 } if len(l.pos) == 2 && action == "set" && strings.HasPrefix(strings.TrimSpace(l.pos[1]), "{") { return call{}, usageErr("a whole settings file is not set at once any more: set each key, " + "nox settings set --node --why (mesh-config.config)") } if len(l.pos) != want || !l.has("node") || !l.has("why") { return call{}, usageErr("give the module, the key%s, --node and --why", map[bool]string{true: " and the value"}[action == "set"]) } a := map[string]any{"action": action, "module": l.pos[0], "key": l.pos[1], "node": l.flags["node"], "why": l.flags["why"]} if action == "set" { a["value"] = jsonOrText(l.pos[2]) } return call{"mesh-config.config", a}, nil } // jsonOrText is a value as JSON when it reads as JSON (a number, true, an object), else the text itself. func jsonOrText(v string) any { var out any if err := json.Unmarshal([]byte(v), &out); err == nil { return out } return v } func retirement(address string) func(line) (call, error) { return func(l line) (call, error) { if len(l.pos) == 0 || !l.has("why") { return call{}, usageErr("give the consumers waiting and --why") } consumers := make([]any, len(l.pos)) for i, c := range l.pos { consumers[i] = c } return call{address, map[string]any{"consumers": consumers, "why": l.flags["why"]}}, nil } } func enrolIssue(l line) (call, error) { if len(l.pos) != 1 || !l.has("why") { return call{}, usageErr("give the new machine's name and --why") } a := pick(l, "why", "expires") a["node"] = l.pos[0] return call{"mesh-enrolment.issue", a}, nil } func addressCall(address string, rest []string) (call, error) { a := map[string]any{} if len(rest) == 1 { if err := json.Unmarshal([]byte(rest[0]), &a); err != nil { return call{}, usageErr("the arguments are not a JSON object: %v", err) } } return call{address, a}, nil } // renderNodes shapes placement's keys as one line per node: its name, address and site. func renderNodes(text string) string { var p struct { Keys map[string]struct { Placement struct { Decided map[string]any `json:"decided"` Modules []any `json:"modules"` } `json:"placement"` } `json:"keys"` } // The console says after the answer which machine answered it ("answered by ace"): kept, below the lines. dec := json.NewDecoder(strings.NewReader(text)) if err := dec.Decode(&p); err != nil || len(p.Keys) == 0 { return text } after := "" if rest, err := io.ReadAll(dec.Buffered()); err == nil { after = strings.TrimSpace(string(rest)) } names := make([]string, 0, len(p.Keys)) for n := range p.Keys { names = append(names, n) } sort.Strings(names) var b strings.Builder for _, n := range names { k := p.Keys[n] fmt.Fprintf(&b, "%-10s %-15v %-8v %d modules\n", n, orDash(k.Placement.Decided["address"]), orDash(k.Placement.Decided["site"]), len(k.Placement.Modules)) } if after != "" { b.WriteString(after + "\n") } return b.String() } func orDash(v any) any { if v == nil || v == "" { return "-" } return v }