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.
This commit is contained in:
Nitya Dhanushkodi 2023-02-10 11:12:13 -08:00 committed by GitHub
parent 318ba215ab
commit 80fb18aa35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 208 additions and 43 deletions

View File

@ -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()}
}

View File

@ -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()
}

88
command/cli/formatting.go Normal file
View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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")
}
}

View File

@ -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, ", ")
}

View File

@ -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")
}
}

4
go.mod
View File

@ -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

4
go.sum
View File

@ -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=