Refactor CLI: birdc-style help, collapsed nouns, ReloadConfig, bug fixes
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'.
This commit is contained in:
@@ -27,58 +27,55 @@ func buildTree() *Node {
|
||||
exit := &Node{Word: "exit", Help: "exit the shell", Run: runQuit}
|
||||
|
||||
// show version
|
||||
showVersion := &Node{Word: "version", Help: "show build version", Run: runShowVersion}
|
||||
showVersion := &Node{Word: "version", Help: "Show build version", Run: runShowVersion}
|
||||
|
||||
// show frontends
|
||||
showFrontends := &Node{Word: "frontends", Help: "list all frontends", Run: runShowFrontends}
|
||||
// show frontend <name>
|
||||
// show frontends [<name>] — without name: list all, with name: show details
|
||||
showFrontendName := &Node{
|
||||
Word: "<name>",
|
||||
Help: "frontend name",
|
||||
Help: "Show details for a single frontend",
|
||||
Dynamic: dynFrontends,
|
||||
Run: runShowFrontend,
|
||||
}
|
||||
showFrontend := &Node{
|
||||
Word: "frontend",
|
||||
Help: "show a single frontend",
|
||||
showFrontends := &Node{
|
||||
Word: "frontends",
|
||||
Help: "List all frontends",
|
||||
Run: runShowFrontends,
|
||||
Children: []*Node{showFrontendName},
|
||||
}
|
||||
|
||||
// show backends
|
||||
showBackends := &Node{Word: "backends", Help: "list all backends", Run: runShowBackends}
|
||||
// show backend <name>
|
||||
// show backends [<name>] — without name: list all, with name: show details
|
||||
showBackendName := &Node{
|
||||
Word: "<name>",
|
||||
Help: "backend name",
|
||||
Help: "Show details for a single backend",
|
||||
Dynamic: dynBackends,
|
||||
Run: runShowBackend,
|
||||
}
|
||||
showBackend := &Node{
|
||||
Word: "backend",
|
||||
Help: "show a single backend",
|
||||
showBackends := &Node{
|
||||
Word: "backends",
|
||||
Help: "List all backends",
|
||||
Run: runShowBackends,
|
||||
Children: []*Node{showBackendName},
|
||||
}
|
||||
|
||||
// show healthchecks
|
||||
showHealthChecks := &Node{Word: "healthchecks", Help: "list all health checks", Run: runShowHealthChecks}
|
||||
// show healthcheck <name>
|
||||
// show healthchecks [<name>] — without name: list all, with name: show details
|
||||
showHealthCheckName := &Node{
|
||||
Word: "<name>",
|
||||
Help: "health check name",
|
||||
Help: "Show details for a single health check",
|
||||
Dynamic: dynHealthChecks,
|
||||
Run: runShowHealthCheck,
|
||||
}
|
||||
showHealthCheck := &Node{
|
||||
Word: "healthcheck",
|
||||
Help: "show a single health check",
|
||||
showHealthChecks := &Node{
|
||||
Word: "healthchecks",
|
||||
Help: "List all health checks",
|
||||
Run: runShowHealthChecks,
|
||||
Children: []*Node{showHealthCheckName},
|
||||
}
|
||||
|
||||
show.Children = []*Node{
|
||||
showVersion,
|
||||
showFrontends, showFrontend,
|
||||
showBackends, showBackend,
|
||||
showHealthChecks, showHealthCheck,
|
||||
showFrontends,
|
||||
showBackends,
|
||||
showHealthChecks,
|
||||
}
|
||||
|
||||
// set backend <name> pause|resume|disabled|enabled
|
||||
@@ -99,9 +96,10 @@ func buildTree() *Node {
|
||||
}
|
||||
// set frontend <name> pool <pool> backend <name> weight <0-100>
|
||||
setWeightValue := &Node{
|
||||
Word: "<weight>",
|
||||
Help: "weight 0-100",
|
||||
Run: runSetFrontendPoolBackendWeight,
|
||||
Word: "<weight>",
|
||||
Help: "Set weight of a backend in a pool (0-100)",
|
||||
Dynamic: dynNone, // accepts any integer; no tab-completion candidates
|
||||
Run: runSetFrontendPoolBackendWeight,
|
||||
}
|
||||
setFrontendPoolBackendWeight := &Node{Word: "weight", Help: "set backend weight in pool", Children: []*Node{setWeightValue}}
|
||||
setFrontendPoolBackendName := &Node{
|
||||
@@ -114,6 +112,7 @@ func buildTree() *Node {
|
||||
setFrontendPoolName := &Node{
|
||||
Word: "<pool>",
|
||||
Help: "pool name",
|
||||
Dynamic: dynNone, // pool names aren't listed via gRPC; accepts any input
|
||||
Children: []*Node{setFrontendPoolBackend},
|
||||
}
|
||||
setFrontendPool := &Node{Word: "pool", Help: "select a pool", Children: []*Node{setFrontendPoolName}}
|
||||
@@ -139,7 +138,7 @@ func buildTree() *Node {
|
||||
var watchEventsOptSlot *Node
|
||||
watchEventsOptSlot = &Node{
|
||||
Word: "<opt>",
|
||||
Help: "watch option",
|
||||
Help: "Stream events with options",
|
||||
Dynamic: dynWatchEventOpts,
|
||||
Run: runWatchEvents,
|
||||
}
|
||||
@@ -157,12 +156,13 @@ func buildTree() *Node {
|
||||
Children: []*Node{watchEvents},
|
||||
}
|
||||
|
||||
// config check
|
||||
configCheck := &Node{Word: "check", Help: "check configuration file", Run: runConfigCheck}
|
||||
// config check / reload
|
||||
configCheck := &Node{Word: "check", Help: "Check configuration file", Run: runConfigCheck}
|
||||
configReload := &Node{Word: "reload", Help: "Check and reload configuration", Run: runConfigReload}
|
||||
configNode := &Node{
|
||||
Word: "config",
|
||||
Help: "configuration commands",
|
||||
Children: []*Node{configCheck},
|
||||
Children: []*Node{configCheck, configReload},
|
||||
}
|
||||
|
||||
root.Children = []*Node{show, set, watch, configNode, quit, exit}
|
||||
@@ -195,6 +195,10 @@ func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string
|
||||
return resp.Names
|
||||
}
|
||||
|
||||
// dynNone marks a slot node that accepts any input but provides no
|
||||
// tab-completion candidates (e.g. a pool name or numeric weight value).
|
||||
func dynNone(_ context.Context, _ grpcapi.MaglevClient) []string { return nil }
|
||||
|
||||
// ---- run functions ---------------------------------------------------------
|
||||
|
||||
func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
|
||||
@@ -315,9 +319,14 @@ func runShowBackend(ctx context.Context, client grpcapi.MaglevClient, args []str
|
||||
fmt.Fprintf(w, "%s\t%s\n", label("healthcheck"), info.Healthcheck)
|
||||
for i, t := range info.Transitions {
|
||||
ts := time.Unix(0, t.AtUnixNs)
|
||||
lbl := ""
|
||||
var lbl string
|
||||
if i == 0 {
|
||||
lbl = label("transitions")
|
||||
} else {
|
||||
// Pad to same visible width as "transitions" and wrap through
|
||||
// label() so tabwriter sees the same byte count (ANSI overhead
|
||||
// is identical on every row, keeping columns aligned).
|
||||
lbl = label(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n",
|
||||
lbl,
|
||||
@@ -501,6 +510,26 @@ func runConfigCheck(ctx context.Context, client grpcapi.MaglevClient, _ []string
|
||||
return fmt.Errorf("semantic error: %s", resp.SemanticError)
|
||||
}
|
||||
|
||||
func runConfigReload(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, callTimeout)
|
||||
defer cancel()
|
||||
resp, err := client.ReloadConfig(ctx, &grpcapi.ReloadConfigRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Ok {
|
||||
fmt.Println("config reloaded")
|
||||
return nil
|
||||
}
|
||||
if resp.ParseError != "" {
|
||||
return fmt.Errorf("parse error: %s", resp.ParseError)
|
||||
}
|
||||
if resp.SemanticError != "" {
|
||||
return fmt.Errorf("semantic error: %s", resp.SemanticError)
|
||||
}
|
||||
return fmt.Errorf("reload error: %s", resp.ReloadError)
|
||||
}
|
||||
|
||||
// formatDuration formats a duration as Xd Xh Xm Xs without milliseconds.
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < 0 {
|
||||
|
||||
Reference in New Issue
Block a user