From 07497dcac3f6f6c0844b77de96891555bd3c48af Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Fri, 15 Nov 2024 10:30:31 -0800 Subject: [PATCH 1/6] feat: add output format flag to the k0s sysinfo command Signed-off-by: Ethan Mosbaugh --- cmd/sysinfo/sysinfo.go | 178 +++++++++++++++++++++++++++--------- cmd/sysinfo/sysinfo_test.go | 148 +++++++++++++++++++++++------- 2 files changed, 250 insertions(+), 76 deletions(-) diff --git a/cmd/sysinfo/sysinfo.go b/cmd/sysinfo/sysinfo.go index 70671d9e972e..c9e6fd131d6e 100644 --- a/cmd/sysinfo/sysinfo.go +++ b/cmd/sysinfo/sysinfo.go @@ -17,7 +17,9 @@ limitations under the License. package sysinfo import ( + "encoding/json" "errors" + "fmt" "io" "strings" @@ -28,11 +30,13 @@ import ( "github.com/logrusorgru/aurora/v3" "github.com/spf13/cobra" "k8s.io/kubectl/pkg/util/term" + "sigs.k8s.io/yaml" ) func NewSysinfoCmd() *cobra.Command { var sysinfoSpec sysinfo.K0sSysinfoSpec + var outputFormat string cmd := &cobra.Command{ Use: "sysinfo", @@ -43,18 +47,18 @@ func NewSysinfoCmd() *cobra.Command { probes := sysinfoSpec.NewSysinfoProbes() out := cmd.OutOrStdout() cli := &cliReporter{ - w: out, colors: aurora.NewAurora(term.IsTerminal(out)), } - if err := probes.Probe(cli); err != nil { return err } + if err := cli.printResults(out, outputFormat); err != nil { + return err + } if cli.failed { return errors.New("sysinfo failed") } - return nil }, } @@ -64,68 +68,157 @@ func NewSysinfoCmd() *cobra.Command { flags.BoolVar(&sysinfoSpec.ControllerRoleEnabled, "controller", true, "Include controller-specific sysinfo") flags.BoolVar(&sysinfoSpec.WorkerRoleEnabled, "worker", true, "Include worker-specific sysinfo") flags.StringVar(&sysinfoSpec.DataDir, "data-dir", constant.DataDirDefault, "Data Directory for k0s") + flags.StringVarP(&outputFormat, "output", "o", "human", "Output format. Must be one of human|yaml|json") return cmd } type cliReporter struct { - w io.Writer - colors aurora.Aurora - failed bool + results []Probe + colors aurora.Aurora + failed bool +} + +type Probe struct { + Path []string + DisplayName string + Prop string + Message string + Category ProbeCategory + Error error } +type ProbeCategory string + +const ( + ProbeCategoryPass ProbeCategory = "pass" + ProbeCategoryWarning ProbeCategory = "warning" + ProbeCategoryRejected ProbeCategory = "rejected" + ProbeCategoryError ProbeCategory = "error" +) + func (r *cliReporter) Pass(p probes.ProbeDesc, v probes.ProbedProp) error { - prop := propString(v) - return r.printf("%s%s%s%s\n", - indent(p), - r.colors.BrightWhite(p.DisplayName()+": "), - r.colors.Green(prop), - buildMsg(prop, "pass", ""), - ) + r.results = append(r.results, Probe{ + Path: probePath(p), + DisplayName: p.DisplayName(), + Prop: propString(v), + Category: ProbeCategoryPass, + }) + return nil } func (r *cliReporter) Warn(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { - prop := propString(v) - return r.printf("%s%s%s%s\n", - indent(p), - r.colors.BrightWhite(p.DisplayName()+": "), - r.colors.Yellow(prop), - buildMsg(prop, "warning", msg)) + r.results = append(r.results, Probe{ + Path: probePath(p), + DisplayName: p.DisplayName(), + Prop: propString(v), + Message: msg, + Category: ProbeCategoryWarning, + }) + return nil } func (r *cliReporter) Reject(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { r.failed = true - prop := propString(v) - return r.printf("%s%s%s%s\n", - indent(p), - r.colors.BrightWhite(p.DisplayName()+": "), - r.colors.Bold(r.colors.Red(prop)), - buildMsg(prop, "rejected", msg)) + r.results = append(r.results, Probe{ + Path: probePath(p), + DisplayName: p.DisplayName(), + Prop: propString(v), + Message: msg, + Category: ProbeCategoryRejected, + }) + return nil } func (r *cliReporter) Error(p probes.ProbeDesc, err error) error { r.failed = true + r.results = append(r.results, Probe{ + Path: probePath(p), + DisplayName: p.DisplayName(), + Category: ProbeCategoryError, + Error: err, + }) + return nil +} - errStr := "error" - if err != nil { - e := err.Error() - if e != "" { - errStr = errStr + ": " + e +func (r *cliReporter) printResults(w io.Writer, outputFormat string) error { + switch outputFormat { + case "human": + return r.printHuman(w) + case "json": + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(r.results) + case "yaml": + b, err := yaml.Marshal(r.results) + if err != nil { + return err } + _, err = io.WriteString(w, string(b)) + return err + default: + return fmt.Errorf("unknown output format: %q", outputFormat) } +} - return r.printf("%s%s%s\n", - indent(p), - r.colors.BrightWhite(p.DisplayName()+": "), - r.colors.Bold(errStr).Red(), - ) +func (r *cliReporter) printHuman(w io.Writer) error { + for _, p := range r.results { + if err := r.printOneHuman(w, p); err != nil { + return err + } + } + return nil } -func (r *cliReporter) printf(format interface{}, args ...interface{}) error { - _, err := io.WriteString(r.w, aurora.Sprintf(format, args...)) +func (r *cliReporter) printOneHuman(w io.Writer, p Probe) error { + var out string + switch p.Category { + case ProbeCategoryPass: + out = aurora.Sprintf("%s%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Green(p.Prop), + buildMsg(p.Prop, string(p.Category), p.Message)) + case ProbeCategoryWarning: + out = aurora.Sprintf("%s%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Yellow(p.Prop), + buildMsg(p.Prop, string(p.Category), p.Message)) + case ProbeCategoryRejected: + out = aurora.Sprintf("%s%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Bold(r.colors.Red(p.Prop)), + buildMsg(p.Prop, string(p.Category), p.Message)) + case ProbeCategoryError: + errStr := "error" + if p.Error != nil { + e := p.Error.Error() + if e != "" { + errStr = errStr + ": " + e + } + } + + out = aurora.Sprintf("%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Bold(errStr).Red(), + ) + default: + return fmt.Errorf("unknown probe category %q", p.Category) + } + _, err := io.WriteString(w, out) return err } +func probePath(p probes.ProbeDesc) []string { + if len(p.Path()) == 0 { + return nil + } + return p.Path() +} + func propString(p probes.ProbedProp) string { if p == nil { return "" @@ -134,13 +227,10 @@ func propString(p probes.ProbedProp) string { return p.String() } -func indent(p probes.ProbeDesc) string { - count := 0 - if p != nil { - count = len(p.Path()) - 1 - if count < 1 { - return "" - } +func indent(path []string) string { + count := len(path) - 1 + if count < 1 { + return "" } return strings.Repeat(" ", count) diff --git a/cmd/sysinfo/sysinfo_test.go b/cmd/sysinfo/sysinfo_test.go index c4dfa911c158..7b4a677675d5 100644 --- a/cmd/sysinfo/sysinfo_test.go +++ b/cmd/sysinfo/sysinfo_test.go @@ -27,13 +27,6 @@ import ( ) func TestCliReporter_Pass(t *testing.T) { - var buf strings.Builder - - underTest := &cliReporter{ - w: &buf, - colors: aurora.NewAurora(true), - } - for _, data := range []struct { name string desc probes.ProbeDesc @@ -67,10 +60,15 @@ func TestCliReporter_Pass(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - buf.Reset() + underTest := &cliReporter{ + colors: aurora.NewAurora(true), + } err := underTest.Pass(data.desc, data.prop) assert.NoError(t, err) assert.False(t, underTest.failed) + var buf strings.Builder + err = underTest.printHuman(&buf) + assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -79,13 +77,6 @@ func TestCliReporter_Pass(t *testing.T) { } func TestCliReporter_Warn(t *testing.T) { - var buf strings.Builder - - underTest := &cliReporter{ - w: &buf, - colors: aurora.NewAurora(true), - } - for _, data := range []struct { name string desc probes.ProbeDesc @@ -120,10 +111,15 @@ func TestCliReporter_Warn(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - buf.Reset() + underTest := &cliReporter{ + colors: aurora.NewAurora(true), + } err := underTest.Warn(data.desc, data.prop, data.msg) assert.NoError(t, err) assert.False(t, underTest.failed) + var buf strings.Builder + err = underTest.printHuman(&buf) + assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -132,13 +128,6 @@ func TestCliReporter_Warn(t *testing.T) { } func TestCliReporter_Reject(t *testing.T) { - var buf strings.Builder - - underTest := &cliReporter{ - w: &buf, - colors: aurora.NewAurora(true), - } - for _, data := range []struct { name string desc probes.ProbeDesc @@ -173,10 +162,15 @@ func TestCliReporter_Reject(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - buf.Reset() + underTest := &cliReporter{ + colors: aurora.NewAurora(true), + } err := underTest.Reject(data.desc, data.prop, data.msg) assert.NoError(t, err) assert.True(t, underTest.failed) + var buf strings.Builder + err = underTest.printHuman(&buf) + assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -185,13 +179,6 @@ func TestCliReporter_Reject(t *testing.T) { } func TestCliReporter_Error(t *testing.T) { - var buf strings.Builder - - underTest := &cliReporter{ - w: &buf, - colors: aurora.NewAurora(true), - } - for _, data := range []struct { name string desc probes.ProbeDesc @@ -215,10 +202,15 @@ func TestCliReporter_Error(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - buf.Reset() + underTest := &cliReporter{ + colors: aurora.NewAurora(true), + } err := underTest.Error(data.desc, data.err) assert.NoError(t, err) assert.True(t, underTest.failed) + var buf strings.Builder + err = underTest.printHuman(&buf) + assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -226,6 +218,98 @@ func TestCliReporter_Error(t *testing.T) { } } +func TestCliReporter(t *testing.T) { + for _, data := range []struct { + name string + probe func(t *testing.T, cli *cliReporter) + xpectResults []Probe + xpect string + xpectFailed bool + }{ + { + "success", + func(t *testing.T, cli *cliReporter) { + err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) + assert.NoError(t, err) + err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) + assert.NoError(t, err) + err = cli.Warn(&testDesc{"foo", nil}, testProp("bar"), "baz") + assert.NoError(t, err) + }, + []Probe{ + {Path: []string{"bar"}, DisplayName: "foo", Prop: "baz", Message: "", Category: "pass", Error: nil}, + {Path: []string{"bar", "baz"}, DisplayName: "foo", Prop: "qux", Message: "", Category: "pass", Error: nil}, + {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "warning", Error: nil}, + }, + ("\x1b[97mfoo: \x1b[0m\x1b[32mbaz\x1b[0m (pass)\n" + + " \x1b[97mfoo: \x1b[0m\x1b[32mqux\x1b[0m (pass)\n" + + "\x1b[97mfoo: \x1b[0m\x1b[33mbar\x1b[0m (warning: baz)\n"), + true, + }, + { + "has_reject", + func(t *testing.T, cli *cliReporter) { + err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) + assert.NoError(t, err) + err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) + assert.NoError(t, err) + err = cli.Warn(&testDesc{"foo", nil}, testProp("bar"), "baz") + assert.NoError(t, err) + err = cli.Reject(&testDesc{"foo", nil}, testProp("bar"), "baz") + assert.NoError(t, err) + }, + []Probe{ + {Path: []string{"bar"}, DisplayName: "foo", Prop: "baz", Message: "", Category: "pass", Error: nil}, + {Path: []string{"bar", "baz"}, DisplayName: "foo", Prop: "qux", Message: "", Category: "pass", Error: nil}, + {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "warning", Error: nil}, + {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "rejected", Error: nil}, + }, + ("\x1b[97mfoo: \x1b[0m\x1b[32mbaz\x1b[0m (pass)\n" + + " \x1b[97mfoo: \x1b[0m\x1b[32mqux\x1b[0m (pass)\n" + + "\x1b[97mfoo: \x1b[0m\x1b[33mbar\x1b[0m (warning: baz)\n" + + "\x1b[97mfoo: \x1b[0m\x1b[1;31mbar\x1b[0m (rejected: baz)\n"), + true, + }, + { + "has_error", + func(t *testing.T, cli *cliReporter) { + err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) + assert.NoError(t, err) + err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) + assert.NoError(t, err) + err = cli.Warn(&testDesc{"foo", nil}, testProp("bar"), "baz") + assert.NoError(t, err) + err = cli.Error(&testDesc{"foo", probes.ProbePath{}}, errors.New("bar")) + assert.NoError(t, err) + }, + []Probe{ + {Path: []string{"bar"}, DisplayName: "foo", Prop: "baz", Message: "", Category: "pass", Error: nil}, + {Path: []string{"bar", "baz"}, DisplayName: "foo", Prop: "qux", Message: "", Category: "pass", Error: nil}, + {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "warning", Error: nil}, + {Path: []string(nil), DisplayName: "foo", Prop: "", Message: "", Category: "error", Error: errors.New("bar")}, + }, + ("\x1b[97mfoo: \x1b[0m\x1b[32mbaz\x1b[0m (pass)\n" + + " \x1b[97mfoo: \x1b[0m\x1b[32mqux\x1b[0m (pass)\n" + + "\x1b[97mfoo: \x1b[0m\x1b[33mbar\x1b[0m (warning: baz)\n" + + "\x1b[97mfoo: \x1b[0m\x1b[1;31merror: bar\x1b[0m\n"), + true, + }, + } { + t.Run(data.name, func(t *testing.T) { + underTest := &cliReporter{ + colors: aurora.NewAurora(true), + } + data.probe(t, underTest) + assert.Equal(t, data.xpectResults, underTest.results) + var buf strings.Builder + err := underTest.printResults(&buf, "human") + assert.NoError(t, err) + result := buf.String() + assert.Equal(t, data.xpect, result) + }) + } +} + type testDesc struct { name string path probes.ProbePath From 621033ccf727d9e8b9c2b5b10c28e9a729f144af Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Sat, 16 Nov 2024 06:45:13 -0800 Subject: [PATCH 2/6] separate reporters implementations for sysinfo cli Signed-off-by: Ethan Mosbaugh --- cmd/sysinfo/sysinfo.go | 194 +++++++++++++++++++++--------------- cmd/sysinfo/sysinfo_test.go | 28 +++--- 2 files changed, 127 insertions(+), 95 deletions(-) diff --git a/cmd/sysinfo/sysinfo.go b/cmd/sysinfo/sysinfo.go index c9e6fd131d6e..91797600b6ea 100644 --- a/cmd/sysinfo/sysinfo.go +++ b/cmd/sysinfo/sysinfo.go @@ -46,17 +46,29 @@ func NewSysinfoCmd() *cobra.Command { sysinfoSpec.AddDebugProbes = true probes := sysinfoSpec.NewSysinfoProbes() out := cmd.OutOrStdout() - cli := &cliReporter{ - colors: aurora.NewAurora(term.IsTerminal(out)), + + var cli cliReporter + switch outputFormat { + case "human": + cli = &humanReporter{ + colors: aurora.NewAurora(term.IsTerminal(out)), + } + case "json": + cli = &jsonReporter{} + case "yaml": + cli = &yamlReporter{} + default: + return fmt.Errorf("unknown output format: %q", outputFormat) } + if err := probes.Probe(cli); err != nil { return err } - if err := cli.printResults(out, outputFormat); err != nil { + if err := cli.printResults(out); err != nil { return err } - if cli.failed { + if cli.isFailed() { return errors.New("sysinfo failed") } return nil @@ -73,10 +85,92 @@ func NewSysinfoCmd() *cobra.Command { return cmd } -type cliReporter struct { - results []Probe - colors aurora.Aurora - failed bool +type cliReporter interface { + probes.Reporter + isFailed() bool + printResults(io.Writer) error +} + +type humanReporter struct { + resultsCollector + colors aurora.Aurora +} + +func (r *humanReporter) printResults(w io.Writer) error { + for _, p := range r.results { + if err := r.printOneHuman(w, p); err != nil { + return err + } + } + return nil +} + +func (r *humanReporter) printOneHuman(w io.Writer, p Probe) error { + var out string + switch p.Category { + case ProbeCategoryPass: + out = aurora.Sprintf("%s%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Green(p.Prop), + buildMsg(p.Prop, string(p.Category), p.Message)) + case ProbeCategoryWarning: + out = aurora.Sprintf("%s%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Yellow(p.Prop), + buildMsg(p.Prop, string(p.Category), p.Message)) + case ProbeCategoryRejected: + out = aurora.Sprintf("%s%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Bold(r.colors.Red(p.Prop)), + buildMsg(p.Prop, string(p.Category), p.Message)) + case ProbeCategoryError: + errStr := "error" + if p.Error != nil { + e := p.Error.Error() + if e != "" { + errStr = errStr + ": " + e + } + } + + out = aurora.Sprintf("%s%s%s\n", + indent(p.Path), + r.colors.BrightWhite(p.DisplayName+": "), + r.colors.Bold(errStr).Red(), + ) + default: + return fmt.Errorf("unknown probe category %q", p.Category) + } + _, err := io.WriteString(w, out) + return err +} + +type jsonReporter struct { + resultsCollector +} + +func (r *jsonReporter) printResults(w io.Writer) error { + jsn, err := json.MarshalIndent(r.results, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprintln(w, string(jsn)) + return err +} + +type yamlReporter struct { + resultsCollector +} + +func (r *yamlReporter) printResults(w io.Writer) error { + ym, err := yaml.Marshal(r.results) + if err != nil { + return err + } + _, err = fmt.Fprintln(w, string(ym)) + return err } type Probe struct { @@ -97,7 +191,12 @@ const ( ProbeCategoryError ProbeCategory = "error" ) -func (r *cliReporter) Pass(p probes.ProbeDesc, v probes.ProbedProp) error { +type resultsCollector struct { + results []Probe + failed bool +} + +func (r *resultsCollector) Pass(p probes.ProbeDesc, v probes.ProbedProp) error { r.results = append(r.results, Probe{ Path: probePath(p), DisplayName: p.DisplayName(), @@ -107,7 +206,7 @@ func (r *cliReporter) Pass(p probes.ProbeDesc, v probes.ProbedProp) error { return nil } -func (r *cliReporter) Warn(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { +func (r *resultsCollector) Warn(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { r.results = append(r.results, Probe{ Path: probePath(p), DisplayName: p.DisplayName(), @@ -118,7 +217,7 @@ func (r *cliReporter) Warn(p probes.ProbeDesc, v probes.ProbedProp, msg string) return nil } -func (r *cliReporter) Reject(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { +func (r *resultsCollector) Reject(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { r.failed = true r.results = append(r.results, Probe{ Path: probePath(p), @@ -130,7 +229,7 @@ func (r *cliReporter) Reject(p probes.ProbeDesc, v probes.ProbedProp, msg string return nil } -func (r *cliReporter) Error(p probes.ProbeDesc, err error) error { +func (r *resultsCollector) Error(p probes.ProbeDesc, err error) error { r.failed = true r.results = append(r.results, Probe{ Path: probePath(p), @@ -141,75 +240,8 @@ func (r *cliReporter) Error(p probes.ProbeDesc, err error) error { return nil } -func (r *cliReporter) printResults(w io.Writer, outputFormat string) error { - switch outputFormat { - case "human": - return r.printHuman(w) - case "json": - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - return enc.Encode(r.results) - case "yaml": - b, err := yaml.Marshal(r.results) - if err != nil { - return err - } - _, err = io.WriteString(w, string(b)) - return err - default: - return fmt.Errorf("unknown output format: %q", outputFormat) - } -} - -func (r *cliReporter) printHuman(w io.Writer) error { - for _, p := range r.results { - if err := r.printOneHuman(w, p); err != nil { - return err - } - } - return nil -} - -func (r *cliReporter) printOneHuman(w io.Writer, p Probe) error { - var out string - switch p.Category { - case ProbeCategoryPass: - out = aurora.Sprintf("%s%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Green(p.Prop), - buildMsg(p.Prop, string(p.Category), p.Message)) - case ProbeCategoryWarning: - out = aurora.Sprintf("%s%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Yellow(p.Prop), - buildMsg(p.Prop, string(p.Category), p.Message)) - case ProbeCategoryRejected: - out = aurora.Sprintf("%s%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Bold(r.colors.Red(p.Prop)), - buildMsg(p.Prop, string(p.Category), p.Message)) - case ProbeCategoryError: - errStr := "error" - if p.Error != nil { - e := p.Error.Error() - if e != "" { - errStr = errStr + ": " + e - } - } - - out = aurora.Sprintf("%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Bold(errStr).Red(), - ) - default: - return fmt.Errorf("unknown probe category %q", p.Category) - } - _, err := io.WriteString(w, out) - return err +func (r *resultsCollector) isFailed() bool { + return r.failed } func probePath(p probes.ProbeDesc) []string { diff --git a/cmd/sysinfo/sysinfo_test.go b/cmd/sysinfo/sysinfo_test.go index 7b4a677675d5..2a034ecca937 100644 --- a/cmd/sysinfo/sysinfo_test.go +++ b/cmd/sysinfo/sysinfo_test.go @@ -60,14 +60,14 @@ func TestCliReporter_Pass(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &cliReporter{ + underTest := &humanReporter{ colors: aurora.NewAurora(true), } err := underTest.Pass(data.desc, data.prop) assert.NoError(t, err) assert.False(t, underTest.failed) var buf strings.Builder - err = underTest.printHuman(&buf) + err = underTest.printResults(&buf) assert.NoError(t, err) result := buf.String() t.Log(result) @@ -111,14 +111,14 @@ func TestCliReporter_Warn(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &cliReporter{ + underTest := &humanReporter{ colors: aurora.NewAurora(true), } err := underTest.Warn(data.desc, data.prop, data.msg) assert.NoError(t, err) assert.False(t, underTest.failed) var buf strings.Builder - err = underTest.printHuman(&buf) + err = underTest.printResults(&buf) assert.NoError(t, err) result := buf.String() t.Log(result) @@ -162,14 +162,14 @@ func TestCliReporter_Reject(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &cliReporter{ + underTest := &humanReporter{ colors: aurora.NewAurora(true), } err := underTest.Reject(data.desc, data.prop, data.msg) assert.NoError(t, err) assert.True(t, underTest.failed) var buf strings.Builder - err = underTest.printHuman(&buf) + err = underTest.printResults(&buf) assert.NoError(t, err) result := buf.String() t.Log(result) @@ -202,14 +202,14 @@ func TestCliReporter_Error(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &cliReporter{ + underTest := &humanReporter{ colors: aurora.NewAurora(true), } err := underTest.Error(data.desc, data.err) assert.NoError(t, err) assert.True(t, underTest.failed) var buf strings.Builder - err = underTest.printHuman(&buf) + err = underTest.printResults(&buf) assert.NoError(t, err) result := buf.String() t.Log(result) @@ -221,14 +221,14 @@ func TestCliReporter_Error(t *testing.T) { func TestCliReporter(t *testing.T) { for _, data := range []struct { name string - probe func(t *testing.T, cli *cliReporter) + probe func(t *testing.T, cli cliReporter) xpectResults []Probe xpect string xpectFailed bool }{ { "success", - func(t *testing.T, cli *cliReporter) { + func(t *testing.T, cli cliReporter) { err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) assert.NoError(t, err) err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) @@ -248,7 +248,7 @@ func TestCliReporter(t *testing.T) { }, { "has_reject", - func(t *testing.T, cli *cliReporter) { + func(t *testing.T, cli cliReporter) { err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) assert.NoError(t, err) err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) @@ -272,7 +272,7 @@ func TestCliReporter(t *testing.T) { }, { "has_error", - func(t *testing.T, cli *cliReporter) { + func(t *testing.T, cli cliReporter) { err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) assert.NoError(t, err) err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) @@ -296,13 +296,13 @@ func TestCliReporter(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &cliReporter{ + underTest := &humanReporter{ colors: aurora.NewAurora(true), } data.probe(t, underTest) assert.Equal(t, data.xpectResults, underTest.results) var buf strings.Builder - err := underTest.printResults(&buf, "human") + err := underTest.printResults(&buf) assert.NoError(t, err) result := buf.String() assert.Equal(t, data.xpect, result) From 2f876786a9adb9ebb989db0517afbeddc6ffe29b Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 26 Nov 2024 10:43:30 -0800 Subject: [PATCH 3/6] sysinfo output flag improvements Signed-off-by: Ethan Mosbaugh --- cmd/sysinfo/sysinfo.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/sysinfo/sysinfo.go b/cmd/sysinfo/sysinfo.go index 91797600b6ea..fb6c64e8b20b 100644 --- a/cmd/sysinfo/sysinfo.go +++ b/cmd/sysinfo/sysinfo.go @@ -49,7 +49,7 @@ func NewSysinfoCmd() *cobra.Command { var cli cliReporter switch outputFormat { - case "human": + case "text": cli = &humanReporter{ colors: aurora.NewAurora(term.IsTerminal(out)), } @@ -80,7 +80,7 @@ func NewSysinfoCmd() *cobra.Command { flags.BoolVar(&sysinfoSpec.ControllerRoleEnabled, "controller", true, "Include controller-specific sysinfo") flags.BoolVar(&sysinfoSpec.WorkerRoleEnabled, "worker", true, "Include worker-specific sysinfo") flags.StringVar(&sysinfoSpec.DataDir, "data-dir", constant.DataDirDefault, "Data Directory for k0s") - flags.StringVarP(&outputFormat, "output", "o", "human", "Output format. Must be one of human|yaml|json") + flags.StringVarP(&outputFormat, "output", "o", "text", "Output format (valid values: text, json, yaml)") return cmd } From c36c9a70f4d8efde7ea892de6e41386584df3aea Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 26 Nov 2024 10:57:50 -0800 Subject: [PATCH 4/6] refactor sysinfo collector for structured formatting Signed-off-by: Ethan Mosbaugh --- cmd/sysinfo/sysinfo.go | 170 +++++++++++++++++------------------- cmd/sysinfo/sysinfo_test.go | 118 ++++++++++++------------- 2 files changed, 132 insertions(+), 156 deletions(-) diff --git a/cmd/sysinfo/sysinfo.go b/cmd/sysinfo/sysinfo.go index fb6c64e8b20b..52ba18fcfb1d 100644 --- a/cmd/sysinfo/sysinfo.go +++ b/cmd/sysinfo/sysinfo.go @@ -47,31 +47,31 @@ func NewSysinfoCmd() *cobra.Command { probes := sysinfoSpec.NewSysinfoProbes() out := cmd.OutOrStdout() - var cli cliReporter switch outputFormat { case "text": - cli = &humanReporter{ + cli := &cliReporter{ + w: out, colors: aurora.NewAurora(term.IsTerminal(out)), } + if err := probes.Probe(cli); err != nil { + return err + } + if cli.failed { + return errors.New("sysinfo failed") + } + return nil + case "json": - cli = &jsonReporter{} + return collectAndPrint(probes, out, func(v interface{}) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + }) + case "yaml": - cli = &yamlReporter{} + return collectAndPrint(probes, out, yaml.Marshal) + default: return fmt.Errorf("unknown output format: %q", outputFormat) } - - if err := probes.Probe(cli); err != nil { - return err - } - if err := cli.printResults(out); err != nil { - return err - } - - if cli.isFailed() { - return errors.New("sysinfo failed") - } - return nil }, } @@ -85,91 +85,78 @@ func NewSysinfoCmd() *cobra.Command { return cmd } -type cliReporter interface { - probes.Reporter - isFailed() bool - printResults(io.Writer) error -} - -type humanReporter struct { - resultsCollector +type cliReporter struct { + w io.Writer colors aurora.Aurora + failed bool } -func (r *humanReporter) printResults(w io.Writer) error { - for _, p := range r.results { - if err := r.printOneHuman(w, p); err != nil { - return err - } - } - return nil +func (r *cliReporter) Pass(p probes.ProbeDesc, v probes.ProbedProp) error { + prop := propString(v) + return r.printf("%s%s%s%s\n", + indent(p), + r.colors.BrightWhite(p.DisplayName()+": "), + r.colors.Green(prop), + buildMsg(prop, "pass", ""), + ) } -func (r *humanReporter) printOneHuman(w io.Writer, p Probe) error { - var out string - switch p.Category { - case ProbeCategoryPass: - out = aurora.Sprintf("%s%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Green(p.Prop), - buildMsg(p.Prop, string(p.Category), p.Message)) - case ProbeCategoryWarning: - out = aurora.Sprintf("%s%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Yellow(p.Prop), - buildMsg(p.Prop, string(p.Category), p.Message)) - case ProbeCategoryRejected: - out = aurora.Sprintf("%s%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Bold(r.colors.Red(p.Prop)), - buildMsg(p.Prop, string(p.Category), p.Message)) - case ProbeCategoryError: - errStr := "error" - if p.Error != nil { - e := p.Error.Error() - if e != "" { - errStr = errStr + ": " + e - } - } - - out = aurora.Sprintf("%s%s%s\n", - indent(p.Path), - r.colors.BrightWhite(p.DisplayName+": "), - r.colors.Bold(errStr).Red(), - ) - default: - return fmt.Errorf("unknown probe category %q", p.Category) - } - _, err := io.WriteString(w, out) - return err +func (r *cliReporter) Warn(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { + prop := propString(v) + return r.printf("%s%s%s%s\n", + indent(p), + r.colors.BrightWhite(p.DisplayName()+": "), + r.colors.Yellow(prop), + buildMsg(prop, "warning", msg)) } -type jsonReporter struct { - resultsCollector +func (r *cliReporter) Reject(p probes.ProbeDesc, v probes.ProbedProp, msg string) error { + r.failed = true + prop := propString(v) + return r.printf("%s%s%s%s\n", + indent(p), + r.colors.BrightWhite(p.DisplayName()+": "), + r.colors.Bold(r.colors.Red(prop)), + buildMsg(prop, "rejected", msg)) } -func (r *jsonReporter) printResults(w io.Writer) error { - jsn, err := json.MarshalIndent(r.results, "", " ") +func (r *cliReporter) Error(p probes.ProbeDesc, err error) error { + r.failed = true + + errStr := "error" if err != nil { - return err + e := err.Error() + if e != "" { + errStr = errStr + ": " + e + } } - _, err = fmt.Fprintln(w, string(jsn)) - return err -} -type yamlReporter struct { - resultsCollector + return r.printf("%s%s%s\n", + indent(p), + r.colors.BrightWhite(p.DisplayName()+": "), + r.colors.Bold(errStr).Red(), + ) } -func (r *yamlReporter) printResults(w io.Writer) error { - ym, err := yaml.Marshal(r.results) +func collectAndPrint(probe probes.Probe, out io.Writer, marshal func(any) ([]byte, error)) error { + var c resultsCollector + if err := probe.Probe(&c); err != nil { + return err + } + if c.failed { + return errors.New("sysinfo failed") + } + bytes, err := marshal(c.results) if err != nil { return err } - _, err = fmt.Fprintln(w, string(ym)) + + _, err = out.Write(bytes) + return err +} + +func (r *cliReporter) printf(format interface{}, args ...interface{}) error { + _, err := io.WriteString(r.w, aurora.Sprintf(format, args...)) return err } @@ -240,10 +227,6 @@ func (r *resultsCollector) Error(p probes.ProbeDesc, err error) error { return nil } -func (r *resultsCollector) isFailed() bool { - return r.failed -} - func probePath(p probes.ProbeDesc) []string { if len(p.Path()) == 0 { return nil @@ -259,10 +242,13 @@ func propString(p probes.ProbedProp) string { return p.String() } -func indent(path []string) string { - count := len(path) - 1 - if count < 1 { - return "" +func indent(p probes.ProbeDesc) string { + count := 0 + if p != nil { + count = len(p.Path()) - 1 + if count < 1 { + return "" + } } return strings.Repeat(" ", count) diff --git a/cmd/sysinfo/sysinfo_test.go b/cmd/sysinfo/sysinfo_test.go index 2a034ecca937..c0b9d035e250 100644 --- a/cmd/sysinfo/sysinfo_test.go +++ b/cmd/sysinfo/sysinfo_test.go @@ -27,6 +27,13 @@ import ( ) func TestCliReporter_Pass(t *testing.T) { + var buf strings.Builder + + underTest := &cliReporter{ + w: &buf, + colors: aurora.NewAurora(true), + } + for _, data := range []struct { name string desc probes.ProbeDesc @@ -60,15 +67,10 @@ func TestCliReporter_Pass(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &humanReporter{ - colors: aurora.NewAurora(true), - } + buf.Reset() err := underTest.Pass(data.desc, data.prop) assert.NoError(t, err) assert.False(t, underTest.failed) - var buf strings.Builder - err = underTest.printResults(&buf) - assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -77,6 +79,13 @@ func TestCliReporter_Pass(t *testing.T) { } func TestCliReporter_Warn(t *testing.T) { + var buf strings.Builder + + underTest := &cliReporter{ + w: &buf, + colors: aurora.NewAurora(true), + } + for _, data := range []struct { name string desc probes.ProbeDesc @@ -111,15 +120,10 @@ func TestCliReporter_Warn(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &humanReporter{ - colors: aurora.NewAurora(true), - } + buf.Reset() err := underTest.Warn(data.desc, data.prop, data.msg) assert.NoError(t, err) assert.False(t, underTest.failed) - var buf strings.Builder - err = underTest.printResults(&buf) - assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -128,6 +132,13 @@ func TestCliReporter_Warn(t *testing.T) { } func TestCliReporter_Reject(t *testing.T) { + var buf strings.Builder + + underTest := &cliReporter{ + w: &buf, + colors: aurora.NewAurora(true), + } + for _, data := range []struct { name string desc probes.ProbeDesc @@ -162,15 +173,10 @@ func TestCliReporter_Reject(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &humanReporter{ - colors: aurora.NewAurora(true), - } + buf.Reset() err := underTest.Reject(data.desc, data.prop, data.msg) assert.NoError(t, err) assert.True(t, underTest.failed) - var buf strings.Builder - err = underTest.printResults(&buf) - assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -179,6 +185,13 @@ func TestCliReporter_Reject(t *testing.T) { } func TestCliReporter_Error(t *testing.T) { + var buf strings.Builder + + underTest := &cliReporter{ + w: &buf, + colors: aurora.NewAurora(true), + } + for _, data := range []struct { name string desc probes.ProbeDesc @@ -202,15 +215,10 @@ func TestCliReporter_Error(t *testing.T) { }, } { t.Run(data.name, func(t *testing.T) { - underTest := &humanReporter{ - colors: aurora.NewAurora(true), - } + buf.Reset() err := underTest.Error(data.desc, data.err) assert.NoError(t, err) assert.True(t, underTest.failed) - var buf strings.Builder - err = underTest.printResults(&buf) - assert.NoError(t, err) result := buf.String() t.Log(result) assert.Equal(t, data.xpect, result) @@ -218,17 +226,28 @@ func TestCliReporter_Error(t *testing.T) { } } -func TestCliReporter(t *testing.T) { +type testDesc struct { + name string + path probes.ProbePath +} + +func (d *testDesc) Path() probes.ProbePath { return d.path } +func (d *testDesc) DisplayName() string { return d.name } + +type testProp string + +func (p testProp) String() string { return string(p) } + +func Test_resultsCollector(t *testing.T) { for _, data := range []struct { name string - probe func(t *testing.T, cli cliReporter) + probe func(t *testing.T, cli *resultsCollector) xpectResults []Probe - xpect string xpectFailed bool }{ { "success", - func(t *testing.T, cli cliReporter) { + func(t *testing.T, cli *resultsCollector) { err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) assert.NoError(t, err) err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) @@ -241,14 +260,11 @@ func TestCliReporter(t *testing.T) { {Path: []string{"bar", "baz"}, DisplayName: "foo", Prop: "qux", Message: "", Category: "pass", Error: nil}, {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "warning", Error: nil}, }, - ("\x1b[97mfoo: \x1b[0m\x1b[32mbaz\x1b[0m (pass)\n" + - " \x1b[97mfoo: \x1b[0m\x1b[32mqux\x1b[0m (pass)\n" + - "\x1b[97mfoo: \x1b[0m\x1b[33mbar\x1b[0m (warning: baz)\n"), - true, + false, }, { "has_reject", - func(t *testing.T, cli cliReporter) { + func(t *testing.T, cli *resultsCollector) { err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) assert.NoError(t, err) err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) @@ -264,15 +280,11 @@ func TestCliReporter(t *testing.T) { {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "warning", Error: nil}, {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "rejected", Error: nil}, }, - ("\x1b[97mfoo: \x1b[0m\x1b[32mbaz\x1b[0m (pass)\n" + - " \x1b[97mfoo: \x1b[0m\x1b[32mqux\x1b[0m (pass)\n" + - "\x1b[97mfoo: \x1b[0m\x1b[33mbar\x1b[0m (warning: baz)\n" + - "\x1b[97mfoo: \x1b[0m\x1b[1;31mbar\x1b[0m (rejected: baz)\n"), true, }, { "has_error", - func(t *testing.T, cli cliReporter) { + func(t *testing.T, cli *resultsCollector) { err := cli.Pass(&testDesc{"foo", probes.ProbePath{"bar"}}, testProp("baz")) assert.NoError(t, err) err = cli.Pass(&testDesc{"foo", probes.ProbePath{"bar", "baz"}}, testProp("qux")) @@ -288,36 +300,14 @@ func TestCliReporter(t *testing.T) { {Path: []string(nil), DisplayName: "foo", Prop: "bar", Message: "baz", Category: "warning", Error: nil}, {Path: []string(nil), DisplayName: "foo", Prop: "", Message: "", Category: "error", Error: errors.New("bar")}, }, - ("\x1b[97mfoo: \x1b[0m\x1b[32mbaz\x1b[0m (pass)\n" + - " \x1b[97mfoo: \x1b[0m\x1b[32mqux\x1b[0m (pass)\n" + - "\x1b[97mfoo: \x1b[0m\x1b[33mbar\x1b[0m (warning: baz)\n" + - "\x1b[97mfoo: \x1b[0m\x1b[1;31merror: bar\x1b[0m\n"), true, }, } { t.Run(data.name, func(t *testing.T) { - underTest := &humanReporter{ - colors: aurora.NewAurora(true), - } - data.probe(t, underTest) - assert.Equal(t, data.xpectResults, underTest.results) - var buf strings.Builder - err := underTest.printResults(&buf) - assert.NoError(t, err) - result := buf.String() - assert.Equal(t, data.xpect, result) + c := &resultsCollector{} + data.probe(t, c) + assert.Equal(t, data.xpectResults, c.results) + assert.Equal(t, data.xpectFailed, c.failed) }) } } - -type testDesc struct { - name string - path probes.ProbePath -} - -func (d *testDesc) Path() probes.ProbePath { return d.path } -func (d *testDesc) DisplayName() string { return d.name } - -type testProp string - -func (p testProp) String() string { return string(p) } From c8f7089b555a21fb28fbef8d136d6e84dcd05712 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 26 Nov 2024 11:13:12 -0800 Subject: [PATCH 5/6] print sysinfo results even if failure Signed-off-by: Ethan Mosbaugh --- cmd/sysinfo/sysinfo.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/sysinfo/sysinfo.go b/cmd/sysinfo/sysinfo.go index 52ba18fcfb1d..3a1ec7fe3ca3 100644 --- a/cmd/sysinfo/sysinfo.go +++ b/cmd/sysinfo/sysinfo.go @@ -143,16 +143,18 @@ func collectAndPrint(probe probes.Probe, out io.Writer, marshal func(any) ([]byt if err := probe.Probe(&c); err != nil { return err } - if c.failed { - return errors.New("sysinfo failed") - } bytes, err := marshal(c.results) if err != nil { return err } - _, err = out.Write(bytes) - return err + if err != nil { + return err + } + if c.failed { + return errors.New("sysinfo failed") + } + return nil } func (r *cliReporter) printf(format interface{}, args ...interface{}) error { From df2649fe208c34d5fb725eb01097d9d7d58a1be5 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 26 Nov 2024 13:01:02 -0800 Subject: [PATCH 6/6] add json tags to structured sysinfo command output Signed-off-by: Ethan Mosbaugh --- cmd/sysinfo/sysinfo.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/sysinfo/sysinfo.go b/cmd/sysinfo/sysinfo.go index 3a1ec7fe3ca3..36e108b1933c 100644 --- a/cmd/sysinfo/sysinfo.go +++ b/cmd/sysinfo/sysinfo.go @@ -163,12 +163,12 @@ func (r *cliReporter) printf(format interface{}, args ...interface{}) error { } type Probe struct { - Path []string - DisplayName string - Prop string - Message string - Category ProbeCategory - Error error + Path []string `json:"path"` + DisplayName string `json:"displayName"` + Prop string `json:"prop"` + Message string `json:"message"` + Category ProbeCategory `json:"category"` + Error error `json:"error"` } type ProbeCategory string