refactor(maglevc): build the CLI on the golang-cli library

Replace maglevc's hand-rolled command-tree CLI with
git.ipng.ch/ipng/golang-cli v1.3.0, mirroring the evpnc refactor. The
tree (commands.go) and the gRPC-status error unwrap (color.go) stay
app-specific; the generic parts — parse tree, completion, '?'-help, the
readline shell, one-shot dispatch, color helpers, and the watch keypress
handler — now come from the library.

- main.go: a single cli.App[grpcapi.MaglevClient] with a Connect
  callback; drops the flag/color-default/dispatch boilerplate.
- commands.go: `type node = cli.Node[grpcapi.MaglevClient]`; label() ->
  cli.Label(); dyn* gain the captured-args parameter the library's
  Dynamic signature carries; runQuit returns cli.ErrQuit.
- watch.go: keypress.WaitForKey replaces the inline cbreak helper.
- color.go: only formatError remains, reading cli.ColorEnabled().
- delete tree.go, complete.go, shell.go.
- tests use the library API; add TestTreeValid (cli.Validate).

Behavior is unchanged except labels/errors now use the library's bright
ANSI palette (was dark); escape lengths are identical so tabwriter
alignment is unaffected. maglevc additionally gains the OpenBSD readline
fix and BSD-correct watch keypress it previously lacked. Builds on linux
and openbsd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 22:38:34 +02:00
parent d2ee6d009e
commit 76fbe2eee0
10 changed files with 227 additions and 788 deletions
+141 -135
View File
@@ -11,82 +11,88 @@ import (
"text/tabwriter"
"time"
cli "git.ipng.ch/ipng/golang-cli"
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
)
const callTimeout = 10 * time.Second
// buildTree constructs the full command tree.
func buildTree() *Node {
root := &Node{Word: "", Help: ""}
// node is the maglevc command tree node: a cli.Node specialized to the maglevd
// gRPC client. The alias keeps the tree literals below concise (&node{...}
// instead of &cli.Node[grpcapi.MaglevClient]{...}).
type node = cli.Node[grpcapi.MaglevClient]
show := &Node{Word: "show", Help: "show information"}
set := &Node{Word: "set", Help: "modify configuration"}
quit := &Node{Word: "quit", Help: "exit the shell", Run: runQuit}
exit := &Node{Word: "exit", Help: "exit the shell", Run: runQuit}
// buildTree constructs the full command tree.
func buildTree() *node {
root := &node{Word: "", Help: ""}
show := &node{Word: "show", Help: "show information"}
set := &node{Word: "set", Help: "modify configuration"}
quit := &node{Word: "quit", Help: "exit the shell", Run: runQuit}
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 [<name>] — without name: list all, with name: show details
showFrontendName := &Node{
showFrontendName := &node{
Word: "<name>",
Help: "Show details for a single frontend",
Dynamic: dynFrontends,
Run: runShowFrontend,
}
showFrontends := &Node{
showFrontends := &node{
Word: "frontends",
Help: "List all frontends",
Run: runShowFrontends,
Children: []*Node{showFrontendName},
Children: []*node{showFrontendName},
}
// show backends [<name>] — without name: list all, with name: show details
showBackendName := &Node{
showBackendName := &node{
Word: "<name>",
Help: "Show details for a single backend",
Dynamic: dynBackends,
Run: runShowBackend,
}
showBackends := &Node{
showBackends := &node{
Word: "backends",
Help: "List all backends",
Run: runShowBackends,
Children: []*Node{showBackendName},
Children: []*node{showBackendName},
}
// show healthchecks [<name>] — without name: list all, with name: show details
showHealthCheckName := &Node{
showHealthCheckName := &node{
Word: "<name>",
Help: "Show details for a single health check",
Dynamic: dynHealthChecks,
Run: runShowHealthCheck,
}
showHealthChecks := &Node{
showHealthChecks := &node{
Word: "healthchecks",
Help: "List all health checks",
Run: runShowHealthChecks,
Children: []*Node{showHealthCheckName},
Children: []*node{showHealthCheckName},
}
// show vpp info / lb state / lb counters
showVPPInfo := &Node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo}
showVPPLBState := &Node{Word: "state", Help: "Show VPP load-balancer state (VIPs and application servers)", Run: runShowVPPLBState}
showVPPLBCounters := &Node{Word: "counters", Help: "Show VPP per-VIP and per-backend packet/byte counters (refreshed every ~5s server-side)", Run: runShowVPPLBCounters}
showVPPLB := &Node{
showVPPInfo := &node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo}
showVPPLBState := &node{Word: "state", Help: "Show VPP load-balancer state (VIPs and application servers)", Run: runShowVPPLBState}
showVPPLBCounters := &node{Word: "counters", Help: "Show VPP per-VIP and per-backend packet/byte counters (refreshed every ~5s server-side)", Run: runShowVPPLBCounters}
showVPPLB := &node{
Word: "lb",
Help: "VPP load-balancer information",
Children: []*Node{showVPPLBState, showVPPLBCounters},
Children: []*node{showVPPLBState, showVPPLBCounters},
}
showVPP := &Node{
showVPP := &node{
Word: "vpp",
Help: "VPP dataplane information",
Children: []*Node{showVPPInfo, showVPPLB},
Children: []*node{showVPPInfo, showVPPLB},
}
show.Children = []*Node{
show.Children = []*node{
showVersion,
showFrontends,
showBackends,
@@ -95,20 +101,20 @@ func buildTree() *Node {
}
// set backend <name> pause|resume|disabled|enabled
setPause := &Node{Word: "pause", Help: "pause health checking", Run: runPauseBackend}
setResume := &Node{Word: "resume", Help: "resume health checking", Run: runResumeBackend}
setDisabled := &Node{Word: "disable", Help: "disable backend (stop probing, remove from rotation)", Run: runDisableBackend}
setEnabled := &Node{Word: "enable", Help: "enable backend (resume probing)", Run: runEnableBackend}
setBackendName := &Node{
setPause := &node{Word: "pause", Help: "pause health checking", Run: runPauseBackend}
setResume := &node{Word: "resume", Help: "resume health checking", Run: runResumeBackend}
setDisabled := &node{Word: "disable", Help: "disable backend (stop probing, remove from rotation)", Run: runDisableBackend}
setEnabled := &node{Word: "enable", Help: "enable backend (resume probing)", Run: runEnableBackend}
setBackendName := &node{
Word: "<name>",
Help: "backend name",
Dynamic: dynBackends,
Children: []*Node{setPause, setResume, setDisabled, setEnabled},
Children: []*node{setPause, setResume, setDisabled, setEnabled},
}
setBackend := &Node{
setBackend := &node{
Word: "backend",
Help: "modify a backend",
Children: []*Node{setBackendName},
Children: []*node{setBackendName},
}
// set frontend <name> pool <pool> backend <name> weight <0-100> [flush]
//
@@ -116,120 +122,120 @@ func buildTree() *Node {
// args, so the literal "flush" keyword isn't visible in the arg
// list. We use two distinct Run functions to distinguish the two
// leaf paths instead — both share the same underlying helper.
setWeightFlush := &Node{
setWeightFlush := &node{
Word: "flush",
Help: "also drop VPP's flow table for this backend (otherwise only the new-buckets map is updated)",
Run: runSetFrontendPoolBackendWeightFlush,
}
setWeightValue := &Node{
setWeightValue := &node{
Word: "<weight>",
Help: "Set weight of a backend in a pool (0-100)",
Dynamic: dynNone, // accepts any integer; no tab-completion candidates
Run: runSetFrontendPoolBackendWeight,
Children: []*Node{setWeightFlush},
Children: []*node{setWeightFlush},
}
setFrontendPoolBackendWeight := &Node{Word: "weight", Help: "set backend weight in pool", Children: []*Node{setWeightValue}}
setFrontendPoolBackendName := &Node{
setFrontendPoolBackendWeight := &node{Word: "weight", Help: "set backend weight in pool", Children: []*node{setWeightValue}}
setFrontendPoolBackendName := &node{
Word: "<backend>",
Help: "backend name",
Dynamic: dynBackends,
Children: []*Node{setFrontendPoolBackendWeight},
Children: []*node{setFrontendPoolBackendWeight},
}
setFrontendPoolBackend := &Node{Word: "backend", Help: "select a backend", Children: []*Node{setFrontendPoolBackendName}}
setFrontendPoolName := &Node{
setFrontendPoolBackend := &node{Word: "backend", Help: "select a backend", Children: []*node{setFrontendPoolBackendName}}
setFrontendPoolName := &node{
Word: "<pool>",
Help: "pool name",
Dynamic: dynNone, // pool names aren't listed via gRPC; accepts any input
Children: []*Node{setFrontendPoolBackend},
Children: []*node{setFrontendPoolBackend},
}
setFrontendPool := &Node{Word: "pool", Help: "select a pool", Children: []*Node{setFrontendPoolName}}
setFrontendName := &Node{
setFrontendPool := &node{Word: "pool", Help: "select a pool", Children: []*node{setFrontendPoolName}}
setFrontendName := &node{
Word: "<name>",
Help: "frontend name",
Dynamic: dynFrontends,
Children: []*Node{setFrontendPool},
Children: []*node{setFrontendPool},
}
setFrontend := &Node{
setFrontend := &node{
Word: "frontend",
Help: "modify a frontend",
Children: []*Node{setFrontendName},
Children: []*node{setFrontendName},
}
set.Children = []*Node{setBackend, setFrontend}
set.Children = []*node{setBackend, setFrontend}
// watch events [num <n>] [log [level <level>]] [backend] [frontend]
//
// All tokens after 'events' are captured as args via a self-referencing slot
// node. This lets runWatchEvents parse the optional flags manually while still
// providing tab-completion through the dynamic enumerator.
watchEventsOptSlot := &Node{
watchEventsOptSlot := &node{
Word: "<opt>",
Help: "Stream events with options",
Dynamic: dynWatchEventOpts,
Run: runWatchEvents,
}
watchEventsOptSlot.Children = []*Node{watchEventsOptSlot}
watchEventsOptSlot.Children = []*node{watchEventsOptSlot}
watchEvents := &Node{
watchEvents := &node{
Word: "events",
Help: "stream events (press any key or Ctrl-C to stop)",
Run: runWatchEvents,
Children: []*Node{watchEventsOptSlot},
Children: []*node{watchEventsOptSlot},
}
watch := &Node{
watch := &node{
Word: "watch",
Help: "watch live event streams",
Children: []*Node{watchEvents},
Children: []*node{watchEvents},
}
// 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{
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, configReload},
Children: []*node{configCheck, configReload},
}
// sync vpp lb state [<name>]
//
// Without a name: run SyncLBStateAll (may remove stale VIPs).
// With a name: run SyncLBStateVIP(name) for just that frontend (no removals).
syncVPPLBStateName := &Node{
syncVPPLBStateName := &node{
Word: "<name>",
Help: "Sync a single frontend's VIP to VPP",
Dynamic: dynFrontends,
Run: runSyncVPPLBState,
}
syncVPPLBState := &Node{
syncVPPLBState := &node{
Word: "state",
Help: "Sync the VPP load-balancer dataplane from the running config",
Run: runSyncVPPLBState,
Children: []*Node{syncVPPLBStateName},
Children: []*node{syncVPPLBStateName},
}
syncVPPLB := &Node{
syncVPPLB := &node{
Word: "lb",
Help: "VPP load-balancer sync commands",
Children: []*Node{syncVPPLBState},
Children: []*node{syncVPPLBState},
}
syncVPP := &Node{
syncVPP := &node{
Word: "vpp",
Help: "VPP dataplane sync commands",
Children: []*Node{syncVPPLB},
Children: []*node{syncVPPLB},
}
syncNode := &Node{
syncNode := &node{
Word: "sync",
Help: "Reconcile dataplane state from the running config",
Children: []*Node{syncVPP},
Children: []*node{syncVPP},
}
root.Children = []*Node{show, set, watch, configNode, syncNode, quit, exit}
root.Children = []*node{show, set, watch, configNode, syncNode, quit, exit}
return root
}
// ---- dynamic enumerators ---------------------------------------------------
func dynFrontends(ctx context.Context, client grpcapi.MaglevClient) []string {
func dynFrontends(ctx context.Context, client grpcapi.MaglevClient, _ []string) []string {
resp, err := client.ListFrontends(ctx, &grpcapi.ListFrontendsRequest{})
if err != nil {
return nil
@@ -237,7 +243,7 @@ func dynFrontends(ctx context.Context, client grpcapi.MaglevClient) []string {
return resp.FrontendNames
}
func dynBackends(ctx context.Context, client grpcapi.MaglevClient) []string {
func dynBackends(ctx context.Context, client grpcapi.MaglevClient, _ []string) []string {
resp, err := client.ListBackends(ctx, &grpcapi.ListBackendsRequest{})
if err != nil {
return nil
@@ -245,7 +251,7 @@ func dynBackends(ctx context.Context, client grpcapi.MaglevClient) []string {
return resp.BackendNames
}
func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string {
func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient, _ []string) []string {
resp, err := client.ListHealthChecks(ctx, &grpcapi.ListHealthChecksRequest{})
if err != nil {
return nil
@@ -255,7 +261,7 @@ func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string
// 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 }
func dynNone(_ context.Context, _ grpcapi.MaglevClient, _ []string) []string { return nil }
// ---- run functions ---------------------------------------------------------
@@ -267,18 +273,18 @@ func runShowVPPInfo(ctx context.Context, client grpcapi.MaglevClient, _ []string
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("version"), info.Version)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("build-date"), info.BuildDate)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("build-dir"), info.BuildDirectory)
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("vpp-pid"), info.Pid)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("version"), info.Version)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("build-date"), info.BuildDate)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("build-dir"), info.BuildDirectory)
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("vpp-pid"), info.Pid)
if info.BoottimeNs > 0 {
bootTime := time.Unix(0, info.BoottimeNs)
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", label("vpp-boottime"),
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", cli.Label("vpp-boottime"),
bootTime.Format("2006-01-02 15:04:05"),
formatDuration(time.Since(bootTime)))
}
connTime := time.Unix(0, info.ConnecttimeNs)
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", label("connected"),
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", cli.Label("connected"),
connTime.Format("2006-01-02 15:04:05"),
formatDuration(time.Since(connTime)))
return w.Flush()
@@ -294,21 +300,21 @@ func runShowVPPLBState(ctx context.Context, client grpcapi.MaglevClient, _ []str
// ---- global config ----
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s\n", label("global"))
_, _ = fmt.Fprintf(w, "%s\n", cli.Label("global"))
if state.Conf.Ip4SrcAddress != "" {
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("ip4-src"), state.Conf.Ip4SrcAddress)
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("ip4-src"), state.Conf.Ip4SrcAddress)
}
if state.Conf.Ip6SrcAddress != "" {
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("ip6-src"), state.Conf.Ip6SrcAddress)
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("ip6-src"), state.Conf.Ip6SrcAddress)
}
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("sticky-buckets-per-core"), state.Conf.StickyBucketsPerCore)
_, _ = fmt.Fprintf(w, " %s\t%ds\n", label("flow-timeout"), state.Conf.FlowTimeout)
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("sticky-buckets-per-core"), state.Conf.StickyBucketsPerCore)
_, _ = fmt.Fprintf(w, " %s\t%ds\n", cli.Label("flow-timeout"), state.Conf.FlowTimeout)
if err := w.Flush(); err != nil {
return err
}
if len(state.Vips) == 0 {
fmt.Println(label("vips") + " (none)")
fmt.Println(cli.Label("vips") + " (none)")
return nil
}
@@ -316,21 +322,21 @@ func runShowVPPLBState(ctx context.Context, client grpcapi.MaglevClient, _ []str
for _, v := range state.Vips {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("vip"), stripHostMask(v.Prefix))
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("protocol"), protoString(v.Protocol))
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("port"), v.Port)
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("encap"), v.Encap)
_, _ = fmt.Fprintf(w, " %s\t%t\n", label("src-ip-sticky"), v.SrcIpSticky)
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("flow-table-length"), v.FlowTableLength)
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("application-servers"), len(v.ApplicationServers))
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("vip"), stripHostMask(v.Prefix))
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("protocol"), protoString(v.Protocol))
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("port"), v.Port)
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("encap"), v.Encap)
_, _ = fmt.Fprintf(w, " %s\t%t\n", cli.Label("src-ip-sticky"), v.SrcIpSticky)
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("flow-table-length"), v.FlowTableLength)
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("application-servers"), len(v.ApplicationServers))
if err := w.Flush(); err != nil {
return err
}
for _, a := range v.ApplicationServers {
fmt.Printf(" %s %s %s %d %s %d\n",
label("address"), a.Address,
label("weight"), a.Weight,
label("flow-table-buckets"), a.NumBuckets)
cli.Label("address"), a.Address,
cli.Label("weight"), a.Weight,
cli.Label("flow-table-buckets"), a.NumBuckets)
}
}
return nil
@@ -356,15 +362,15 @@ func runShowVPPLBCounters(ctx context.Context, client grpcapi.MaglevClient, _ []
// ---- frontend-counters ----
//
// Column headers are plain strings, not label()-wrapped. tabwriter
// Column headers are plain strings, not cli.Label()-wrapped. tabwriter
// counts bytes (not rendered width), so wrapping a header cell in
// ANSI escape codes inflates its apparent width by ~11 bytes and
// the data rows below — which are plain numeric strings — end up
// over-padded. The label() convention only works when every cell
// over-padded. The cli.Label() convention only works when every cell
// in a column shares the same wrapping, which the key-value show
// commands do but this table can't (we're not about to colourise
// every packet count).
fmt.Println(label("frontend-counters"))
fmt.Println(cli.Label("frontend-counters"))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, " vip\tproto\tport\tfirst\tnext\tuntracked\tno-server\tfib-packets\tfib-bytes\n")
for _, v := range resp.Vips {
@@ -429,7 +435,7 @@ func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error
}
func runQuit(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
return errQuit
return cli.ErrQuit
}
func runShowFrontends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
@@ -457,17 +463,17 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("address"), info.Address)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("protocol"), info.Protocol)
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port)
_, _ = fmt.Fprintf(w, "%s\t%t\n", label("src-ip-sticky"), info.SrcIpSticky)
_, _ = fmt.Fprintf(w, "%s\t%t\n", label("flush-on-down"), info.FlushOnDown)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("name"), info.Name)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("address"), info.Address)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("protocol"), info.Protocol)
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("port"), info.Port)
_, _ = fmt.Fprintf(w, "%s\t%t\n", cli.Label("src-ip-sticky"), info.SrcIpSticky)
_, _ = fmt.Fprintf(w, "%s\t%t\n", cli.Label("flush-on-down"), info.FlushOnDown)
if info.Description != "" {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("description"), info.Description)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("description"), info.Description)
}
if len(info.Pools) > 0 {
_, _ = fmt.Fprintf(w, "%s\n", label("pools"))
_, _ = fmt.Fprintf(w, "%s\n", cli.Label("pools"))
}
if err := w.Flush(); err != nil {
return err
@@ -484,7 +490,7 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
for _, pool := range info.Pools {
namePad := strings.Repeat(" ", poolLblWidth-len("name"))
fmt.Printf("%s%s%s%s%s\n", poolIndent, label("name"), namePad, poolSep, pool.Name)
fmt.Printf("%s%s%s%s%s\n", poolIndent, cli.Label("name"), namePad, poolSep, pool.Name)
for i, pb := range pool.Backends {
beInfo, beErr := client.GetBackend(ctx, &grpcapi.GetBackendRequest{Name: pb.Name})
suffix := ""
@@ -496,11 +502,11 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
// after pool-failover logic). Format matches the VPP-style
// key-value line so robot tests can parse it with a regex.
metaStr := fmt.Sprintf(" %s %d %s %d",
label("weight"), pb.Weight,
label("effective"), pb.EffectiveWeight)
cli.Label("weight"), pb.Weight,
cli.Label("effective"), pb.EffectiveWeight)
if i == 0 {
bePad := strings.Repeat(" ", poolLblWidth-len("backends"))
fmt.Printf("%s%s%s%s%s%s%s\n", poolIndent, label("backends"), bePad, poolSep, pb.Name, metaStr, suffix)
fmt.Printf("%s%s%s%s%s%s%s\n", poolIndent, cli.Label("backends"), bePad, poolSep, pb.Name, metaStr, suffix)
} else {
fmt.Printf("%s%s%s%s\n", contIndent, pb.Name, metaStr, suffix)
}
@@ -533,26 +539,26 @@ func runShowBackend(ctx context.Context, client grpcapi.MaglevClient, args []str
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("address"), info.Address)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("name"), info.Name)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("address"), info.Address)
stateDur := ""
if len(info.Transitions) > 0 {
since := time.Since(time.Unix(0, info.Transitions[0].AtUnixNs))
stateDur = " for " + formatDuration(since)
}
_, _ = fmt.Fprintf(w, "%s\t%s%s\n", label("state"), info.State, stateDur)
_, _ = fmt.Fprintf(w, "%s\t%v\n", label("enabled"), info.Enabled)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("healthcheck"), info.Healthcheck)
_, _ = fmt.Fprintf(w, "%s\t%s%s\n", cli.Label("state"), info.State, stateDur)
_, _ = fmt.Fprintf(w, "%s\t%v\n", cli.Label("enabled"), info.Enabled)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("healthcheck"), info.Healthcheck)
for i, t := range info.Transitions {
ts := time.Unix(0, t.AtUnixNs)
var lbl string
if i == 0 {
lbl = label("transitions")
lbl = cli.Label("transitions")
} else {
// Pad to same visible width as "transitions" and wrap through
// label() so tabwriter sees the same byte count (ANSI overhead
// cli.Label() so tabwriter sees the same byte count (ANSI overhead
// is identical on every row, keeping columns aligned).
lbl = label(" ")
lbl = cli.Label(" ")
}
_, _ = fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n",
lbl,
@@ -588,41 +594,41 @@ func runShowHealthCheck(ctx context.Context, client grpcapi.MaglevClient, args [
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("type"), info.Type)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("name"), info.Name)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("type"), info.Type)
if info.Port > 0 {
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port)
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("port"), info.Port)
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("interval"), time.Duration(info.IntervalNs))
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("interval"), time.Duration(info.IntervalNs))
if info.FastIntervalNs > 0 {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("fast-interval"), time.Duration(info.FastIntervalNs))
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("fast-interval"), time.Duration(info.FastIntervalNs))
}
if info.DownIntervalNs > 0 {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("down-interval"), time.Duration(info.DownIntervalNs))
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("down-interval"), time.Duration(info.DownIntervalNs))
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("timeout"), time.Duration(info.TimeoutNs))
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("rise"), info.Rise)
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("fall"), info.Fall)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("timeout"), time.Duration(info.TimeoutNs))
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("rise"), info.Rise)
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("fall"), info.Fall)
if info.ProbeIpv4Src != "" {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv4-src"), info.ProbeIpv4Src)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("probe-ipv4-src"), info.ProbeIpv4Src)
}
if info.ProbeIpv6Src != "" {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv6-src"), info.ProbeIpv6Src)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("probe-ipv6-src"), info.ProbeIpv6Src)
}
if h := info.Http; h != nil {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("http.path"), h.Path)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("http.path"), h.Path)
if h.Host != "" {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("http.host"), h.Host)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("http.host"), h.Host)
}
_, _ = fmt.Fprintf(w, "%s\t%d-%d\n", label("http.response-code"), h.ResponseCodeMin, h.ResponseCodeMax)
_, _ = fmt.Fprintf(w, "%s\t%d-%d\n", cli.Label("http.response-code"), h.ResponseCodeMin, h.ResponseCodeMax)
if h.ResponseRegexp != "" {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("http.response-regexp"), h.ResponseRegexp)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("http.response-regexp"), h.ResponseRegexp)
}
}
if t := info.Tcp; t != nil {
_, _ = fmt.Fprintf(w, "%s\t%v\n", label("tcp.ssl"), t.Ssl)
_, _ = fmt.Fprintf(w, "%s\t%v\n", cli.Label("tcp.ssl"), t.Ssl)
if t.ServerName != "" {
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("tcp.server-name"), t.ServerName)
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("tcp.server-name"), t.ServerName)
}
}
return w.Flush()