From 4cb0a6f4f9a743c542f9bc09b064e078c38a816f Mon Sep 17 00:00:00 2001 From: Noah Lavine Date: Fri, 25 Sep 2020 10:27:57 -0400 Subject: [PATCH] Add printJsonTable This makes cnetstat's table-printing functions generic to the type of data being printed. This will let us print either connections or summary statistics using the same code. --- cnetstat.go | 57 +++++++-------------------------------------- print_table.go | 36 ++++++++++++++++++++++++++-- print_table_test.go | 31 +++++++++++++++++++----- 3 files changed, 68 insertions(+), 56 deletions(-) diff --git a/cnetstat.go b/cnetstat.go index eaa4101..f102cd6 100644 --- a/cnetstat.go +++ b/cnetstat.go @@ -23,32 +23,6 @@ type KubeConnection struct { container ContainerPath } -// I'm not using the standard json module for JSON output because I -// want to flatten the KubeConnection before printing it -func writeKubeConnectionAsJSON(kc *KubeConnection, w io.Writer) error { - _, err := fmt.Fprintf(w, - "{\"protocol\": %v,"+ - "\"local_host\": %v, "+ - "\"local_port\": %v, "+ - "\"remote_host\": %v, "+ - "\"remote_port\": %v, "+ - "\"connection_state\": %v, "+ - "\"pod_namespace\": %v, "+ - "\"pod_name\": %v, "+ - "\"container_name\": %v}", - kc.conn.protocol, - kc.conn.localHost, - kc.conn.localPort, - kc.conn.remoteHost, - kc.conn.remotePort, - kc.conn.connectionState, - kc.container.PodNamespace, - kc.container.PodName, - kc.container.ContainerName) - - return err -} - const subprocessTimeout = 5 * time.Second const ppidColon string = "PPid:" @@ -123,14 +97,6 @@ func getKubeConnections(connections []Connection, pidMap map[int]ContainerPath) return kubeConnections } -// Convert empty strings to "-". Why? Because that's what netstat does -func emptyToDash(val string) string { - if len(val) > 0 { - return val - } else { - return "-" - } -} var kubeConnectionHeaders = []string{ "Namespace", "Pod", "Container", "Protocol", @@ -140,9 +106,9 @@ var kubeConnectionHeaders = []string{ func (kc KubeConnection) Fields() []string { return []string{ - emptyToDash(kc.container.PodNamespace), - emptyToDash(kc.container.PodName), - emptyToDash(kc.container.ContainerName), + kc.container.PodNamespace, + kc.container.PodName, + kc.container.ContainerName, kc.conn.protocol, kc.conn.localHost, kc.conn.localPort, @@ -227,20 +193,15 @@ func cnetstat() error { kubeConnections := getKubeConnections(allConnections, pidMap) + table := make([]Fielder, len(kubeConnections)) + for i, _ := range kubeConnections { + table[i] = &kubeConnections[i] + } + switch format { case "json": - for _, conn := range kubeConnections { - err := writeKubeConnectionAsJSON(&conn, os.Stdout) - if err != nil { - return err - } - os.Stdout.WriteString("\n") - } + printJsonTable(table, kubeConnectionHeaders, os.Stdout) case "table": - table := make([]Fielder, len(kubeConnections)) - for i, _ := range kubeConnections { - table[i] = &kubeConnections[i] - } prettyPrintTable(table, kubeConnectionHeaders, os.Stdout) } diff --git a/print_table.go b/print_table.go index c7eec3b..ffbc837 100644 --- a/print_table.go +++ b/print_table.go @@ -18,9 +18,18 @@ func max(a, b int) int { return a } +// Convert empty strings to "-". Why? Because that's what netstat does +func emptyToDash(val string) string { + if len(val) > 0 { + return val + } else { + return "-" + } +} + // Print a table. The fields will print in the order returned by // Fielder.Fields(). header is the first row of fields to print, which -// can be used for column titles. +// can be used for column titles. Empty fields will be printed as "-". func prettyPrintTable(rows []Fielder, header []string, f io.Writer) { w := bufio.NewWriter(f) @@ -49,9 +58,32 @@ func prettyPrintTable(rows []Fielder, header []string, f io.Writer) { w.WriteString("\n") for _, row := range rows { for i, field := range row.Fields() { - fmt.Fprintf(w, "%-*s", fieldWidths[i], field) + fmt.Fprintf(w, "%-*s", fieldWidths[i], emptyToDash(field)) } w.WriteString("\n") } w.Flush() } + +// Print a table as a series of JSON rows, one row per line of +// output. Each row will be a JSON object where property names come +// from header and values come from the row being printed. +func printJsonTable(rows []Fielder, fieldNames []string, f io.Writer) { + w := bufio.NewWriter(f) + + for _, row := range rows { + w.WriteString("{") + for i, field := range row.Fields() { + // Using fprintf with a buffered writer + // results in two buffers, but this still + // seems better than the alternatives. + fmt.Fprintf(w, "\"%s\": \"%s\"", fieldNames[i], field) + if i < (len(fieldNames) - 1) { + w.WriteString(", ") + } + } + w.WriteString("}\n") + } + + w.Flush() +} diff --git a/print_table_test.go b/print_table_test.go index 5fe78a1..bb4e4db 100644 --- a/print_table_test.go +++ b/print_table_test.go @@ -15,22 +15,41 @@ func (t TestTable) Fields() []string { return []string{t.a, t.b, t.c} } +var testTable = []Fielder{ + &TestTable{a: "a", b: "b", c: "cc"}, + &TestTable{a: "aaa", b: "b", c: "c"}, + &TestTable{a: "A", b: "", c: "c"}, +} + +var testFields = []string{"AAA", "B", "C"} + const expectedTable = `AAA B C a b cc aaa b c +A - c ` func TestPrettyPrintTable(t *testing.T) { var buf bytes.Buffer - table := []Fielder{ - &TestTable{a: "a", b: "b", c: "cc"}, - &TestTable{a: "aaa", b: "b", c: "c"}, - } - - prettyPrintTable(table, []string{"AAA", "B", "C"}, &buf) + prettyPrintTable(testTable, testFields, &buf) written := buf.String() if written != expectedTable { t.Errorf("prettyPrintTable wrote %#v, expected %#v", written, expectedTable) } } + +const expectedJson = `{"AAA": "a", "B": "b", "C": "cc"} +{"AAA": "aaa", "B": "b", "C": "c"} +{"AAA": "A", "B": "", "C": "c"} +` + +func testPrintJsonTable(t *testing.T) { + var buf bytes.Buffer + + printJsonTable(testTable, testFields, &buf) + written := buf.String() + if written != expectedJson { + t.Errorf("printJsonTable wrote %#v, expected %#v", written, expectedJson) + } +}