maglevc - Rewrite '?' handler (birdc-style): show full command paths from current position to every leaf, right-aligned help column, dynamic slot values displayed as an indented block when cursor is at a slot position. - Collapse show frontends/frontend, backends/backend, healthchecks/healthcheck into single plural-noun nodes with an optional <name> slot. Allows 'sh ba' (list all) and 'sh ba nginx0' (show one) without ambiguity. - Add 'config reload' command. - Fix tabwriter ANSI alignment: continuation lines in transition output now carry the same label() byte overhead as the header line. - Fix broken Walk for 'set frontend' command: setFrontendPoolName and setWeightValue were fixed-word nodes that couldn't capture user input; mark them as slot nodes with dynNone. - Add tree_test.go covering expandPaths, cycle detection, prefix matching, and the full weight-command walk. gRPC / proto - Add ReloadConfig RPC: checks config then applies it to the running checker, returning ok/parse_error/semantic_error/reload_error. - Add logging to CheckConfig (config-check-start/config-check-done at INFO level). maglevd - SIGHUP handler now calls maglevServer.TriggerReload(), sharing the same code path as the gRPC ReloadConfig RPC. docs - Collapse show command documentation to use [<name>] optional syntax. - Remove developer-facing 'Command tree and parser' section. - Document 'config reload'.
158 lines
4.1 KiB
Go
158 lines
4.1 KiB
Go
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
|
)
|
|
|
|
// Node is one word in the command tree. Leaf nodes have a Run function.
|
|
// Slot nodes have Dynamic set (and no fixed Word to match against); they
|
|
// accept any single token as an argument and may have further Children.
|
|
type Node struct {
|
|
Word string
|
|
Help string
|
|
Dynamic func(context.Context, grpcapi.MaglevClient) []string // non-nil → slot node
|
|
Children []*Node
|
|
Run func(context.Context, grpcapi.MaglevClient, []string) error
|
|
}
|
|
|
|
// Walk descends the tree following tokens. At each step it tries fixed
|
|
// children first (exact then prefix), then falls back to a slot child
|
|
// (Dynamic != nil). Tokens consumed by slot children are collected as args.
|
|
// Returns the deepest node reached and the args collected from slot nodes.
|
|
func Walk(root *Node, tokens []string) (*Node, []string) {
|
|
node := root
|
|
var args []string
|
|
for len(tokens) > 0 {
|
|
tok := tokens[0]
|
|
|
|
// Try fixed children (exact, then unique prefix).
|
|
next := matchFixedChild(node.Children, tok)
|
|
if next != nil {
|
|
node = next
|
|
tokens = tokens[1:]
|
|
continue
|
|
}
|
|
|
|
// Try a slot child.
|
|
slot := findSlotChild(node.Children)
|
|
if slot != nil {
|
|
args = append(args, tok)
|
|
tokens = tokens[1:]
|
|
node = slot
|
|
continue
|
|
}
|
|
|
|
// Dead end — no match.
|
|
break
|
|
}
|
|
return node, args
|
|
}
|
|
|
|
// matchFixedChild returns the child matching tok by exact then unique prefix,
|
|
// considering only non-slot children.
|
|
func matchFixedChild(children []*Node, tok string) *Node {
|
|
var fixed []*Node
|
|
for _, c := range children {
|
|
if c.Dynamic == nil {
|
|
fixed = append(fixed, c)
|
|
}
|
|
}
|
|
// Exact match.
|
|
for _, c := range fixed {
|
|
if c.Word == tok {
|
|
return c
|
|
}
|
|
}
|
|
// Unique prefix match.
|
|
var matches []*Node
|
|
for _, c := range fixed {
|
|
if strings.HasPrefix(c.Word, tok) {
|
|
matches = append(matches, c)
|
|
}
|
|
}
|
|
if len(matches) == 1 {
|
|
return matches[0]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// findSlotChild returns the first child that is a slot node (Dynamic != nil).
|
|
func findSlotChild(children []*Node) *Node {
|
|
for _, c := range children {
|
|
if c.Dynamic != nil {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// helpLine is a (path, help) pair used when displaying '?' output.
|
|
type helpLine struct {
|
|
path string
|
|
help string
|
|
}
|
|
|
|
// expandPaths returns all (path, help) pairs for every node reachable from
|
|
// node that has a Run function. prefix is the display string accumulated so
|
|
// far (e.g. "show frontend"). visited prevents infinite loops through
|
|
// self-referencing slot nodes like watchEventsOptSlot.
|
|
func expandPaths(node *Node, prefix string, visited map[*Node]bool) []helpLine {
|
|
if visited[node] {
|
|
return nil
|
|
}
|
|
visited[node] = true
|
|
|
|
var lines []helpLine
|
|
if node.Run != nil {
|
|
lines = append(lines, helpLine{path: prefix, help: node.Help})
|
|
}
|
|
for _, child := range node.Children {
|
|
childPrefix := child.Word
|
|
if prefix != "" {
|
|
childPrefix = prefix + " " + child.Word
|
|
}
|
|
lines = append(lines, expandPaths(child, childPrefix, visited)...)
|
|
}
|
|
return lines
|
|
}
|
|
|
|
// Candidates returns the completable children at the current position given
|
|
// the already-typed tokens and the partial token being completed.
|
|
func Candidates(root *Node, tokens []string, partial string, ctx context.Context, client grpcapi.MaglevClient) []*Node {
|
|
// Walk the already-confirmed tokens.
|
|
node, _ := Walk(root, tokens)
|
|
|
|
// Now look at what could follow at this node.
|
|
// Check fixed children filtered by partial.
|
|
fixedMatches := filterFixedChildren(node.Children, partial)
|
|
|
|
// Check dynamic slot child if present.
|
|
var dynMatches []*Node
|
|
slot := findSlotChild(node.Children)
|
|
if slot != nil && slot.Dynamic != nil {
|
|
vals := slot.Dynamic(ctx, client)
|
|
for _, v := range vals {
|
|
if strings.HasPrefix(v, partial) {
|
|
dynMatches = append(dynMatches, &Node{Word: v, Help: slot.Help})
|
|
}
|
|
}
|
|
}
|
|
|
|
return append(fixedMatches, dynMatches...)
|
|
}
|
|
|
|
func filterFixedChildren(children []*Node, prefix string) []*Node {
|
|
var out []*Node
|
|
for _, c := range children {
|
|
if c.Dynamic == nil && strings.HasPrefix(c.Word, prefix) {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|