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:
2026-04-11 18:20:43 +02:00
parent 58391f5463
commit 3bd30b69f4
11 changed files with 657 additions and 222 deletions

View File

@@ -204,12 +204,57 @@ func (s *Server) WatchEvents(req *WatchRequest, stream Maglev_WatchEventsServer)
// CheckConfig reads and validates the configuration file, returning a
// structured result that distinguishes YAML parse errors from semantic errors.
func (s *Server) CheckConfig(_ context.Context, _ *CheckConfigRequest) (*CheckConfigResponse, error) {
slog.Info("config-check-start", "path", s.configPath)
_, result := config.Check(s.configPath)
return &CheckConfigResponse{
resp := &CheckConfigResponse{
Ok: result.OK(),
ParseError: result.ParseError,
SemanticError: result.SemanticError,
}, nil
}
if result.OK() {
slog.Info("config-check-done", "result", "ok")
} else if result.ParseError != "" {
slog.Info("config-check-done", "result", "failed", "type", "parse", "err", result.ParseError)
} else {
slog.Info("config-check-done", "result", "failed", "type", "semantic", "err", result.SemanticError)
}
return resp, nil
}
// ReloadConfig checks the configuration file and, if valid, applies it to the
// running checker. This is the same code path used by SIGHUP.
func (s *Server) ReloadConfig(_ context.Context, _ *ReloadConfigRequest) (*ReloadConfigResponse, error) {
return s.doReloadConfig(), nil
}
// TriggerReload performs a config check and reload. Intended for use by the
// SIGHUP handler so that signals and gRPC share the same code path.
func (s *Server) TriggerReload() {
s.doReloadConfig()
}
func (s *Server) doReloadConfig() *ReloadConfigResponse {
slog.Info("config-reload-start")
newCfg, result := config.Check(s.configPath)
if !result.OK() {
if result.ParseError != "" {
slog.Error("config-check-failed", "type", "parse", "err", result.ParseError)
} else {
slog.Error("config-check-failed", "type", "semantic", "err", result.SemanticError)
}
return &ReloadConfigResponse{
ParseError: result.ParseError,
SemanticError: result.SemanticError,
}
}
if err := s.checker.Reload(s.ctx, newCfg); err != nil {
slog.Error("checker-reload-error", "err", err)
return &ReloadConfigResponse{
ReloadError: err.Error(),
}
}
slog.Info("config-reload-done", "frontends", len(newCfg.Frontends))
return &ReloadConfigResponse{Ok: true}
}
// ---- conversion helpers ----------------------------------------------------