From 80fb18aa3581cb41376809c69402dd191bbcd166 Mon Sep 17 00:00:00 2001 From: Nitya Dhanushkodi Date: Fri, 10 Feb 2023 11:12:13 -0800 Subject: [PATCH] troubleshoot: make output have tables and colors (#16235) Adds tables and colors using libraries used in consul-k8s. It doesn't add the full `terminal` UI package that consul-k8s uses since there is an existing UI in Consul that I didn't want to affect too much. So instead this adds to the existing UI. --- command/agent/agent_test.go | 27 +++++- command/cli/cli.go | 67 ++++++++++++++ command/cli/formatting.go | 88 +++++++++++++++++++ .../troubleshoot/proxy/troubleshoot_proxy.go | 9 +- .../proxy/troubleshoot_proxy_test.go | 16 ---- .../upstreams/troubleshoot_upstreams.go | 20 ++++- .../upstreams/troubleshoot_upstreams_test.go | 16 ---- go.mod | 4 +- go.sum | 4 + 9 files changed, 208 insertions(+), 43 deletions(-) create mode 100644 command/cli/formatting.go delete mode 100644 command/troubleshoot/proxy/troubleshoot_proxy_test.go delete mode 100644 command/troubleshoot/upstreams/troubleshoot_upstreams_test.go diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 45fc700a4b..bb6d8837cf 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -9,7 +9,8 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/consul/command/cli" + mcli "github.com/mitchellh/cli" "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/sdk/testutil" @@ -215,7 +216,7 @@ func TestBadDataDirPermissions(t *testing.T) { } type captureUI struct { - *cli.MockUi + *mcli.MockUi } func (c *captureUI) Stdout() io.Writer { @@ -226,6 +227,24 @@ func (c *captureUI) Stderr() io.Writer { return c.MockUi.ErrorWriter } -func newCaptureUI() *captureUI { - return &captureUI{MockUi: cli.NewMockUi()} +func (c *captureUI) HeaderOutput(s string) { +} + +func (c *captureUI) ErrorOutput(s string) { +} + +func (c *captureUI) WarnOutput(s string) { +} + +func (c *captureUI) SuccessOutput(s string) { +} + +func (c *captureUI) UnchangedOutput(s string) { +} + +func (c *captureUI) Table(tbl *cli.Table) { +} + +func newCaptureUI() *captureUI { + return &captureUI{MockUi: mcli.NewMockUi()} } diff --git a/command/cli/cli.go b/command/cli/cli.go index 6a7edd7202..44cbf17b21 100644 --- a/command/cli/cli.go +++ b/command/cli/cli.go @@ -1,8 +1,11 @@ package cli import ( + "fmt" "io" + "github.com/olekukonko/tablewriter" + mcli "github.com/mitchellh/cli" ) @@ -13,6 +16,12 @@ type Ui interface { mcli.Ui Stdout() io.Writer Stderr() io.Writer + ErrorOutput(string) + HeaderOutput(string) + WarnOutput(string) + SuccessOutput(string) + UnchangedOutput(string) + Table(tbl *Table) } // BasicUI augments mitchellh/cli.BasicUi by exposing the underlying io.Writer. @@ -28,5 +37,63 @@ func (b *BasicUI) Stderr() io.Writer { return b.BasicUi.ErrorWriter } +func (b *BasicUI) HeaderOutput(s string) { + b.Output(colorize(fmt.Sprintf("==> %s", s), UiColorNone)) +} + +func (b *BasicUI) ErrorOutput(s string) { + b.Output(colorize(fmt.Sprintf(" ! %s", s), UiColorRed)) +} + +func (b *BasicUI) WarnOutput(s string) { + b.Output(colorize(fmt.Sprintf(" * %s", s), UiColorYellow)) +} + +func (b *BasicUI) SuccessOutput(s string) { + b.Output(colorize(fmt.Sprintf(" ✓ %s", s), UiColorGreen)) +} + +func (b *BasicUI) UnchangedOutput(s string) { + b.Output(colorize(fmt.Sprintf(" %s", s), UiColorNone)) +} + // Command is an alias to reduce the diff. It can be removed at any time. type Command mcli.Command + +// Table implements UI. +func (u *BasicUI) Table(tbl *Table) { + // Build our config and set our options + + table := tablewriter.NewWriter(u.Stdout()) + + table.SetHeader(tbl.Headers) + table.SetBorder(false) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + + for _, row := range tbl.Rows { + colors := make([]tablewriter.Colors, len(row)) + entries := make([]string, len(row)) + + for i, ent := range row { + entries[i] = ent.Value + + color, ok := colorMapping[ent.Color] + if ok { + colors[i] = tablewriter.Colors{color} + } + } + + table.Rich(entries, colors) + } + + table.Render() +} diff --git a/command/cli/formatting.go b/command/cli/formatting.go new file mode 100644 index 0000000000..8df31bb90d --- /dev/null +++ b/command/cli/formatting.go @@ -0,0 +1,88 @@ +package cli + +import ( + "github.com/fatih/color" + "github.com/olekukonko/tablewriter" +) + +// Functions to set up colorized output. +const ( + noColor = -1 +) + +func colorize(message string, uc UiColor) string { + if uc.Code == noColor { + return message + } + + attr := []color.Attribute{color.Attribute(uc.Code)} + if uc.Bold { + attr = append(attr, color.Bold) + } + + return color.New(attr...).SprintFunc()(message) +} + +// UiColor is a posix shell color code to use. +type UiColor struct { + Code int + Bold bool +} + +// A list of colors that are useful. These are all non-bolded by default. +var ( + UiColorNone UiColor = UiColor{noColor, false} + UiColorRed = UiColor{int(color.FgHiRed), false} + UiColorGreen = UiColor{int(color.FgHiGreen), false} + UiColorYellow = UiColor{int(color.FgHiYellow), false} + UiColorBlue = UiColor{int(color.FgHiBlue), false} + UiColorMagenta = UiColor{int(color.FgHiMagenta), false} + UiColorCyan = UiColor{int(color.FgHiCyan), false} +) + +// Functions that are useful for table output. +const ( + Yellow = "yellow" + Green = "green" + Red = "red" +) + +var colorMapping = map[string]int{ + Green: tablewriter.FgGreenColor, + Yellow: tablewriter.FgYellowColor, + Red: tablewriter.FgRedColor, +} + +// Table is passed to UI.Table to provide a nicely formatted table. +type Table struct { + Headers []string + Rows [][]Cell +} + +// Cell is a single entry for a table. +type Cell struct { + Value string + Color string +} + +// NewTable creates a new Table structure that can be used with UI.Table. +func NewTable(headers ...string) *Table { + return &Table{ + Headers: headers, + } +} + +// AddRow adds a row to the table. +func (t *Table) AddRow(cols []string, colors []string) { + var row []Cell + + for i, col := range cols { + if i < len(colors) { + row = append(row, Cell{Value: col, Color: colors[i]}) + } else { + row = append(row, Cell{Value: col}) + } + } + + t.Rows = append(t.Rows, row) +} diff --git a/command/troubleshoot/proxy/troubleshoot_proxy.go b/command/troubleshoot/proxy/troubleshoot_proxy.go index d729830659..8950424a78 100644 --- a/command/troubleshoot/proxy/troubleshoot_proxy.go +++ b/command/troubleshoot/proxy/troubleshoot_proxy.go @@ -6,9 +6,9 @@ import ( "net" "os" + "github.com/hashicorp/consul/command/cli" "github.com/hashicorp/consul/command/flags" troubleshoot "github.com/hashicorp/consul/troubleshoot/proxy" - "github.com/mitchellh/cli" ) func New(ui cli.Ui) *cmd { @@ -86,13 +86,14 @@ func (c *cmd) Run(args []string) int { return 1 } + c.UI.HeaderOutput("Validation") for _, o := range messages { if o.Success { - c.UI.Output(o.Message) + c.UI.SuccessOutput(o.Message) } else { - c.UI.Error(o.Message) + c.UI.ErrorOutput(o.Message) if o.PossibleActions != "" { - c.UI.Output(o.PossibleActions) + c.UI.UnchangedOutput(o.PossibleActions) } } } diff --git a/command/troubleshoot/proxy/troubleshoot_proxy_test.go b/command/troubleshoot/proxy/troubleshoot_proxy_test.go deleted file mode 100644 index 7b88a62aaa..0000000000 --- a/command/troubleshoot/proxy/troubleshoot_proxy_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package proxy - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func TestTroubleshootProxyCommand_noTabs(t *testing.T) { - t.Parallel() - - if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { - t.Fatal("help has tabs") - } -} diff --git a/command/troubleshoot/upstreams/troubleshoot_upstreams.go b/command/troubleshoot/upstreams/troubleshoot_upstreams.go index 116952bf57..1249f5ddc5 100644 --- a/command/troubleshoot/upstreams/troubleshoot_upstreams.go +++ b/command/troubleshoot/upstreams/troubleshoot_upstreams.go @@ -5,10 +5,12 @@ import ( "fmt" "net" "os" + "strconv" + "strings" + "github.com/hashicorp/consul/command/cli" "github.com/hashicorp/consul/command/flags" troubleshoot "github.com/hashicorp/consul/troubleshoot/proxy" - "github.com/mitchellh/cli" ) func New(ui cli.Ui) *cmd { @@ -81,9 +83,11 @@ func (c *cmd) Run(args []string) int { } c.UI.Output(fmt.Sprintf("\n==> Upstream IPs (transparent proxy only) (%v)", len(upstreamIPs))) + tbl := cli.NewTable("IPs ", "Virtual ", "Cluster Names") for _, u := range upstreamIPs { - c.UI.Output(fmt.Sprintf("%+v %v %+v", u.IPs, u.IsVirtual, u.ClusterNames)) + tbl.AddRow([]string{formatIPs(u.IPs), strconv.FormatBool(u.IsVirtual), formatClusterNames(u.ClusterNames)}, []string{}) } + c.UI.Table(tbl) c.UI.Output("\nIf you don't see your upstream address or cluster for a transparent proxy upstream:") c.UI.Output("- Check intentions: Tproxy upstreams are configured based on intentions, make sure you " + @@ -114,3 +118,15 @@ Usage: consul troubleshoot upstreams [options] $ consul troubleshoot upstreams ` ) + +func formatIPs(ips []string) string { + return strings.Join(ips, ", ") +} + +func formatClusterNames(names map[string]struct{}) string { + var out []string + for k := range names { + out = append(out, k) + } + return strings.Join(out, ", ") +} diff --git a/command/troubleshoot/upstreams/troubleshoot_upstreams_test.go b/command/troubleshoot/upstreams/troubleshoot_upstreams_test.go deleted file mode 100644 index e36f5c996c..0000000000 --- a/command/troubleshoot/upstreams/troubleshoot_upstreams_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package upstreams - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func TestTroubleshootUpstreamsCommand_noTabs(t *testing.T) { - t.Parallel() - - if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { - t.Fatal("help has tabs") - } -} diff --git a/go.mod b/go.mod index c4e0a773fb..0a7f18f041 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/coreos/go-oidc v2.1.0+incompatible github.com/docker/go-connections v0.3.0 github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 + github.com/fatih/color v1.13.0 github.com/fsnotify/fsnotify v1.5.1 github.com/go-openapi/runtime v0.24.1 github.com/go-openapi/strfmt v0.21.3 @@ -86,6 +87,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/pointerstructure v1.2.1 github.com/mitchellh/reflectwalk v1.0.2 + github.com/olekukonko/tablewriter v0.0.4 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.14.0 @@ -147,7 +149,6 @@ require ( github.com/digitalocean/godo v1.10.0 // indirect github.com/dimchansky/utfbom v1.1.0 // indirect github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect - github.com/fatih/color v1.13.0 // indirect github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/analysis v0.21.2 // indirect @@ -189,6 +190,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index 394df1a26c..e32c7478f4 100644 --- a/go.sum +++ b/go.sum @@ -743,6 +743,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -815,6 +817,8 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=