go/clitable: factor out from metroctl
We need the same functionality in bmcli, so factor it out from metroctl
into a generic library.
Change-Id: I3fb3dfaae44a64d204e9220f117f379c382c5c4f
Reviewed-on: https://review.monogon.dev/c/monogon/+/2172
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index 111c6e6..e3044d5 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load(":defs.bzl", "buildkind")
buildkind(
@@ -28,7 +28,6 @@
"cmd_takeownership.go",
"main.go",
"rpc.go",
- "table.go",
"table_node.go",
],
data = select({
@@ -41,6 +40,7 @@
importpath = "source.monogon.dev/metropolis/cli/metroctl",
visibility = ["//visibility:private"],
deps = [
+ "//go/clitable",
"//metropolis/cli/metroctl/core",
"//metropolis/cli/pkg/context",
"//metropolis/cli/pkg/datafile",
@@ -64,9 +64,3 @@
embed = [":metroctl_lib"],
visibility = ["//visibility:public"],
)
-
-go_test(
- name = "metroctl_test",
- srcs = ["table_test.go"],
- embed = [":metroctl_lib"],
-)
diff --git a/metropolis/cli/metroctl/cmd_node.go b/metropolis/cli/metroctl/cmd_node.go
index b9692b6..1e4afa5 100644
--- a/metropolis/cli/metroctl/cmd_node.go
+++ b/metropolis/cli/metroctl/cmd_node.go
@@ -11,6 +11,7 @@
"github.com/spf13/cobra"
+ "source.monogon.dev/go/clitable"
"source.monogon.dev/metropolis/cli/metroctl/core"
clicontext "source.monogon.dev/metropolis/cli/pkg/context"
"source.monogon.dev/metropolis/node/core/identity"
@@ -173,7 +174,7 @@
}
}
- var t table
+ var t clitable.Table
for _, n := range nodes {
// Filter the information we want client-side.
if len(qids) != 0 {
@@ -182,8 +183,8 @@
continue
}
}
- t.add(nodeEntry(n))
+ t.Add(nodeEntry(n))
}
- t.print(o, onlyColumns)
+ t.Print(o, onlyColumns)
}
diff --git a/metropolis/cli/metroctl/table.go b/metropolis/cli/metroctl/table.go
deleted file mode 100644
index 1d947d9..0000000
--- a/metropolis/cli/metroctl/table.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package main
-
-import (
- "fmt"
- "io"
- "strings"
-)
-
-// table is a list of entries that form a table with sparse columns. Each entry
-// defines its own columns.
-type table struct {
- entries []entry
-}
-
-// add an entry to the table.
-func (t *table) add(e entry) {
- t.entries = append(t.entries, e)
-}
-
-// An entry is made up of column key -> value pairs.
-type entry struct {
- columns []entryColumn
-}
-
-// add a key/value pair to the entry.
-func (e *entry) add(key, value string) {
- e.columns = append(e.columns, entryColumn{
- key: key,
- value: value,
- })
-}
-
-// get a value from a given key, returning zero string if not set.
-func (e *entry) get(key string) string {
- for _, col := range e.columns {
- if col.key == key {
- return col.value
- }
- }
- return ""
-}
-
-// An entryColumn is a pair for table column key and entry column value.
-type entryColumn struct {
- key string
- value string
-}
-
-// columns returns the keys and widths of columns that are present in the table's
-// entries.
-func (t *table) columns() columns {
- var res columns
- for _, e := range t.entries {
- for _, c := range e.columns {
- tc := res.upsert(c.key)
- if len(c.value) > tc.width {
- tc.width = len(c.value)
- }
- }
- }
- return res
-}
-
-type columns []*column
-
-// A column in a table, not containing all entries that make up this table, but
-// containing their maximum width (for layout purposes).
-type column struct {
- // key is the column key.
- key string
- // width is the maximum width (in runes) of all the entries' data in this column.
- width int
-}
-
-// upsert a key into a list of columns, returning the upserted column.
-func (c *columns) upsert(key string) *column {
- for _, col := range *c {
- if col.key == key {
- return col
- }
- }
- col := &column{
- key: key,
- width: len(key),
- }
- *c = append(*c, col)
- return col
-}
-
-// filter returns a copy of columns where the only columns present are the ones
-// whose onlyColumns values are true. If only columns is nil, no filtering takes
-// place (all columns are returned).
-func (c columns) filter(onlyColumns map[string]bool) columns {
- var res []*column
- for _, cc := range c {
- if onlyColumns != nil && !onlyColumns[cc.key] {
- continue
- }
- res = append(res, cc)
- }
- return res
-}
-
-// printHeader writes a table-like header to the given file, keeping margin
-// spaces between columns.
-func (c columns) printHeader(f io.Writer, margin int) {
- for _, cc := range c {
- fmt.Fprintf(f, "%-*s", cc.width+margin, strings.ToUpper(cc.key))
- }
- fmt.Fprintf(f, "\n")
-}
-
-// print writes a table-like representation of this table to the given file,
-// first filtering the columns by onlyColumns (if not set, no filtering takes
-// place).
-func (t *table) print(f io.Writer, onlyColumns map[string]bool) {
- margin := 3
- cols := t.columns().filter(onlyColumns)
- cols.printHeader(f, margin)
-
- for _, e := range t.entries {
- for _, c := range cols {
- v := e.get(c.key)
- fmt.Fprintf(f, "%-*s", c.width+margin, v)
- }
- fmt.Fprintf(f, "\n")
- }
-}
diff --git a/metropolis/cli/metroctl/table_node.go b/metropolis/cli/metroctl/table_node.go
index c708443..07f994c 100644
--- a/metropolis/cli/metroctl/table_node.go
+++ b/metropolis/cli/metroctl/table_node.go
@@ -5,24 +5,25 @@
"sort"
"strings"
+ "source.monogon.dev/go/clitable"
"source.monogon.dev/metropolis/node/core/identity"
apb "source.monogon.dev/metropolis/proto/api"
cpb "source.monogon.dev/metropolis/proto/common"
)
-func nodeEntry(n *apb.Node) entry {
- res := entry{}
+func nodeEntry(n *apb.Node) clitable.Entry {
+ res := clitable.Entry{}
- res.add("node id", identity.NodeID(n.Pubkey))
+ res.Add("node id", identity.NodeID(n.Pubkey))
state := n.State.String()
state = strings.ReplaceAll(state, "NODE_STATE_", "")
- res.add("state", state)
+ res.Add("state", state)
address := "unknown"
if n.Status != nil && n.Status.ExternalAddress != "" {
address = n.Status.ExternalAddress
}
- res.add("address", address)
- res.add("health", n.Health.String())
+ res.Add("address", address)
+ res.Add("health", n.Health.String())
var roles []string
if n.Roles.ConsensusMember != nil {
@@ -35,7 +36,7 @@
roles = append(roles, "KubernetesWorker")
}
sort.Strings(roles)
- res.add("roles", strings.Join(roles, ","))
+ res.Add("roles", strings.Join(roles, ","))
tpm := "unk"
switch n.TpmUsage {
@@ -46,10 +47,10 @@
case cpb.NodeTPMUsage_NODE_TPM_NOT_PRESENT:
tpm = "no"
}
- res.add("tpm", tpm)
+ res.Add("tpm", tpm)
tshs := n.TimeSinceHeartbeat.GetSeconds()
- res.add("heartbeat", fmt.Sprintf("%ds", tshs))
+ res.Add("heartbeat", fmt.Sprintf("%ds", tshs))
return res
}
diff --git a/metropolis/cli/metroctl/table_test.go b/metropolis/cli/metroctl/table_test.go
deleted file mode 100644
index 0ea1448..0000000
--- a/metropolis/cli/metroctl/table_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package main
-
-import (
- "bytes"
- "strings"
- "testing"
-)
-
-// TestTableLayout performs a smoke test of the table layout functionality.
-func TestTableLayout(t *testing.T) {
- tab := table{}
-
- e := entry{}
- e.add("id", "short")
- e.add("labels", "")
- tab.add(e)
-
- e = entry{}
- e.add("whoops", "only in second")
- e.add("labels", "bar")
- e.add("id", "this one is a very long one")
- tab.add(e)
-
- e = entry{}
- e.add("id", "normal length")
- e.add("labels", "foo")
- tab.add(e)
-
- buf := bytes.NewBuffer(nil)
- tab.print(buf, nil)
-
- golden := `
-ID LABELS WHOOPS
-short
-this one is a very long one bar only in second
-normal length foo
-`
- golden = strings.TrimSpace(golden)
- got := strings.TrimSpace(buf.String())
- if got != golden {
- t.Logf("wanted: \n" + golden)
- t.Logf("got: \n" + got)
- t.Errorf("mismatch")
- }
-}