From db40734a0db89850a2a685c9a7f5f5559875b7b3 Mon Sep 17 00:00:00 2001 From: Thomas Schuetz Date: Wed, 5 Apr 2023 13:36:30 +0200 Subject: [PATCH 1/3] chore: made json output prettier and improved output Signed-off-by: Thomas Schuetz --- cmd/analyze/analyze.go | 66 ++++++++---------------------- pkg/analysis/analysis.go | 86 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 50 deletions(-) diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index 2a3f176c..0a04c407 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -2,19 +2,14 @@ package analyze import ( "context" - "encoding/json" "fmt" - "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" - "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" - "os" - "strings" - "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" + "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" - "github.com/schollz/progressbar/v3" "github.com/spf13/cobra" "github.com/spf13/viper" + "os" ) var ( @@ -79,54 +74,25 @@ var AnalyzeCmd = &cobra.Command{ os.Exit(1) } - if len(config.Results) == 0 { - color.Green("{ \"status\": \"OK\" }") - os.Exit(0) - } - var bar = progressbar.Default(int64(len(config.Results))) - if !explain { - bar.Clear() - } - var printOutput []analyzer.Result - - for _, analysis := range config.Results { - if explain { - parsedText, err := aiClient.Parse(ctx, analysis.Error, nocache) - if err != nil { - // Check for exhaustion - if strings.Contains(err.Error(), "status code: 429") { - color.Red("Exhausted API quota. Please try again later") - os.Exit(1) - } - color.Red("Error: %v", err) - continue - } - analysis.Details = parsedText - bar.Add(1) + if explain && output != "json" { + err := config.GetAIResults(true) + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) } - printOutput = append(printOutput, analysis) } // print results - for n, analysis := range printOutput { - - switch output { - case "json": - analysis.Error = analysis.Error[0:] - j, err := json.Marshal(analysis) - if err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - fmt.Println(string(j)) - default: - fmt.Printf("%s %s(%s)\n", color.CyanString("%d", n), - color.YellowString(analysis.Name), color.CyanString(analysis.ParentObject)) - for _, err := range analysis.Error { - fmt.Printf("- %s %s\n", color.RedString("Error:"), color.RedString(err)) - } - fmt.Println(color.GreenString(analysis.Details + "\n")) + switch output { + case "json": + output, err := config.JsonOutput() + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) } + fmt.Println(string(output)) + default: + config.PrintOutput() } }, } diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index f2dd3451..611edb34 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -2,10 +2,16 @@ package analysis import ( "context" + "encoding/json" + "fmt" + "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/schollz/progressbar/v3" "github.com/spf13/viper" + "os" + "strings" ) type Analysis struct { @@ -19,6 +25,19 @@ type Analysis struct { Explain bool } +type AnalysisStatus string + +const ( + StateOK AnalysisStatus = "OK" + StateProblemDetected AnalysisStatus = "ProblemDetected" +) + +type JsonOutput struct { + Status AnalysisStatus `json:"status"` + Problems int `json:"problems"` + Results []analyzer.Result `json:"results"` +} + func (a *Analysis) RunAnalysis() error { activeFilters := viper.GetStringSlice("active_filters") @@ -70,3 +89,70 @@ func (a *Analysis) RunAnalysis() error { } return nil } + +func (a *Analysis) JsonOutput() ([]byte, error) { + var problems int + var status AnalysisStatus + for _, result := range a.Results { + problems += len(result.Error) + } + if problems > 0 { + status = StateProblemDetected + } else { + status = StateOK + } + + result := JsonOutput{ + Problems: len(a.Results), + Results: a.Results, + Status: status, + } + output, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + return output, nil +} + +func (a *Analysis) PrintOutput() { + fmt.Println("") + if len(a.Results) == 0 { + fmt.Println(color.GreenString("No problems detected")) + } + for n, result := range a.Results { + fmt.Printf("%s %s(%s)\n", color.CyanString("%d", n), + color.YellowString(result.Name), color.CyanString(result.ParentObject)) + for _, err := range result.Error { + fmt.Printf("- %s %s\n", color.RedString("Error:"), color.RedString(err)) + } + fmt.Println(color.GreenString(result.Details + "\n")) + } +} + +func (a *Analysis) GetAIResults(progressBar bool) error { + if len(a.Results) == 0 { + return nil + } + + var bar *progressbar.ProgressBar + if progressBar { + bar = progressbar.Default(int64(len(a.Results))) + } + + for index, analysis := range a.Results { + parsedText, err := a.AIClient.Parse(a.Context, analysis.Error, a.NoCache) + if err != nil { + // Check for exhaustion + if strings.Contains(err.Error(), "status code: 429") { + color.Red("Exhausted API quota. Please try again later") + os.Exit(1) + } + color.Red("Error: %v", err) + continue + } + analysis.Details = parsedText + bar.Add(1) + a.Results[index] = analysis + } + return nil +} From 22e31661bff27b28339898826a34ffdcfcff3583 Mon Sep 17 00:00:00 2001 From: Thomas Schuetz Date: Wed, 5 Apr 2023 14:08:23 +0200 Subject: [PATCH 2/3] chore: added initial tests for json output Signed-off-by: Thomas Schuetz --- go.mod | 2 + pkg/analysis/analysis.go | 2 +- pkg/analysis/analysis_test.go | 123 ++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 pkg/analysis/analysis_test.go diff --git a/go.mod b/go.mod index ab7006cc..a6f10e1d 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/schollz/progressbar/v3 v3.13.1 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.1 golang.org/x/term v0.7.0 k8s.io/api v0.26.3 k8s.io/apimachinery v0.26.3 @@ -46,6 +47,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.0 // indirect diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index 611edb34..0287ed59 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -103,7 +103,7 @@ func (a *Analysis) JsonOutput() ([]byte, error) { } result := JsonOutput{ - Problems: len(a.Results), + Problems: problems, Results: a.Results, Status: status, } diff --git a/pkg/analysis/analysis_test.go b/pkg/analysis/analysis_test.go new file mode 100644 index 00000000..c3ec32c9 --- /dev/null +++ b/pkg/analysis/analysis_test.go @@ -0,0 +1,123 @@ +package analysis + +import ( + "encoding/json" + "fmt" + "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" + "github.com/stretchr/testify/require" + "testing" +) + +func TestAnalysis_NoProblemJsonOutput(t *testing.T) { + + analysis := Analysis{ + Results: []analyzer.Result{}, + Namespace: "default", + } + + expected := JsonOutput{ + Status: StateOK, + Problems: 0, + Results: []analyzer.Result{}, + } + + gotJson, err := analysis.JsonOutput() + if err != nil { + t.Error(err) + } + + got := JsonOutput{} + err = json.Unmarshal(gotJson, &got) + if err != nil { + t.Error(err) + } + + fmt.Println(got) + fmt.Println(expected) + + require.Equal(t, got, expected) +} + +func TestAnalysis_ProblemJsonOutput(t *testing.T) { + analysis := Analysis{ + Results: []analyzer.Result{ + { + "Deployment", + "test-deployment", + []string{"test-problem"}, + "test-solution", + "parent-resource"}, + }, + Namespace: "default", + } + + expected := JsonOutput{ + Status: StateProblemDetected, + Problems: 1, + Results: []analyzer.Result{ + {"Deployment", + "test-deployment", + []string{"test-problem"}, + "test-solution", + "parent-resource"}, + }, + } + + gotJson, err := analysis.JsonOutput() + if err != nil { + t.Error(err) + } + + got := JsonOutput{} + err = json.Unmarshal(gotJson, &got) + if err != nil { + t.Error(err) + } + + fmt.Println(got) + fmt.Println(expected) + + require.Equal(t, got, expected) +} + +func TestAnalysis_MultipleProblemJsonOutput(t *testing.T) { + analysis := Analysis{ + Results: []analyzer.Result{ + { + "Deployment", + "test-deployment", + []string{"test-problem", "another-test-problem"}, + "test-solution", + "parent-resource"}, + }, + Namespace: "default", + } + + expected := JsonOutput{ + Status: StateProblemDetected, + Problems: 2, + Results: []analyzer.Result{ + {"Deployment", + "test-deployment", + []string{"test-problem", "another-test-problem"}, + "test-solution", + "parent-resource"}, + }, + } + + gotJson, err := analysis.JsonOutput() + if err != nil { + t.Error(err) + } + + got := JsonOutput{} + err = json.Unmarshal(gotJson, &got) + if err != nil { + t.Error(err) + } + + fmt.Println(got) + fmt.Println(expected) + + require.Equal(t, got, expected) +} From 2f2100289953af7820bbb01f2c980cf5492de079 Mon Sep 17 00:00:00 2001 From: Thomas Schuetz Date: Wed, 5 Apr 2023 15:17:06 +0200 Subject: [PATCH 3/3] fix: details in json output Signed-off-by: Thomas Schuetz --- cmd/analyze/analyze.go | 4 ++-- pkg/analysis/analysis.go | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index 0a04c407..d534ee44 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -74,8 +74,8 @@ var AnalyzeCmd = &cobra.Command{ os.Exit(1) } - if explain && output != "json" { - err := config.GetAIResults(true) + if explain { + err := config.GetAIResults(output) if err != nil { color.Red("Error: %v", err) os.Exit(1) diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index 0287ed59..ba182f03 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -129,13 +129,13 @@ func (a *Analysis) PrintOutput() { } } -func (a *Analysis) GetAIResults(progressBar bool) error { +func (a *Analysis) GetAIResults(output string) error { if len(a.Results) == 0 { return nil } var bar *progressbar.ProgressBar - if progressBar { + if output != "json" { bar = progressbar.Default(int64(len(a.Results))) } @@ -151,7 +151,9 @@ func (a *Analysis) GetAIResults(progressBar bool) error { continue } analysis.Details = parsedText - bar.Add(1) + if output != "json" { + bar.Add(1) + } a.Results[index] = analysis } return nil