diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index 105ebdf..904f381 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -23,6 +23,7 @@ import ( "github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var ( @@ -67,25 +68,45 @@ var AnalyzeCmd = &cobra.Command{ withStats, ) + verbose := viper.GetBool("verbose") + if verbose { + fmt.Println("Debug: Checking analysis configuration.") + } if err != nil { color.Red("Error: %v", err) os.Exit(1) } + if verbose { + fmt.Println("Debug: Analysis initialized.") + } defer config.Close() if customAnalysis { config.RunCustomAnalysis() + if verbose { + fmt.Println("Debug: All custom analyzers completed.") + } } config.RunAnalysis() + if verbose { + fmt.Println("Debug: All core analyzers completed.") + } if explain { - if err := config.GetAIResults(output, anonymize); err != nil { + err := config.GetAIResults(output, anonymize) + if verbose { + fmt.Println("Debug: Checking AI results.") + } + if err != nil { color.Red("Error: %v", err) os.Exit(1) } } // print results output_data, err := config.PrintOutput(output) + if verbose { + fmt.Println("Debug: Checking output.") + } if err != nil { color.Red("Error: %v", err) os.Exit(1) diff --git a/cmd/root.go b/cmd/root.go index 8ae3754..1bcda3c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -37,6 +37,7 @@ var ( cfgFile string kubecontext string kubeconfig string + verbose bool Version string Commit string Date string @@ -84,6 +85,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("Default config file (%s/k8sgpt/k8sgpt.yaml)", xdg.ConfigHome)) rootCmd.PersistentFlags().StringVar(&kubecontext, "kubecontext", "", "Kubernetes context to use. Only required if out-of-cluster.") rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed tool actions (e.g., API calls, checks).") } // initConfig reads in config file and ENV variables if set. @@ -104,6 +106,7 @@ func initConfig() { viper.Set("kubecontext", kubecontext) viper.Set("kubeconfig", kubeconfig) + viper.Set("verbose", verbose) viper.SetEnvPrefix("K8SGPT") viper.AutomaticEnv() // read in environment variables that match diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..02e3ef0 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,30 @@ +/* +Copyright 2023 The K8sGPT Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" + + "github.com/spf13/viper" +) + +// Test that verbose flag is correctly set in viper. +func TestInitConfig_VerboseFlag(t *testing.T) { + verbose = true + viper.Reset() + initConfig() + if !viper.GetBool("verbose") { + t.Error("Expected verbose flag to be true") + } +} diff --git a/go.mod b/go.mod index a00f581..20ba65b 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 github.com/IBM/watsonx-go v1.0.1 + github.com/agiledragon/gomonkey/v2 v2.13.0 github.com/aws/aws-sdk-go v1.55.6 github.com/cohere-ai/cohere-go/v2 v2.12.2 github.com/go-logr/zapr v1.3.0 @@ -48,7 +49,6 @@ require ( github.com/pterm/pterm v0.12.80 google.golang.org/api v0.218.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/gateway-api v1.2.1 ) @@ -136,6 +136,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect ) diff --git a/go.sum b/go.sum index b3d5412..5070b78 100644 --- a/go.sum +++ b/go.sum @@ -713,6 +713,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo= +github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -1075,6 +1077,7 @@ github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/Q github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= github.com/gophercloud/gophercloud/v2 v2.4.0 h1:XhP5tVEH3ni66NSNK1+0iSO6kaGPH/6srtx6Cr+8eCg= github.com/gophercloud/gophercloud/v2 v2.4.0/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -1152,6 +1155,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -1382,6 +1386,8 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -1802,6 +1808,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index bed85b1..3f83fa7 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -18,6 +18,7 @@ import ( "encoding/base64" "errors" "fmt" + "reflect" "strings" "sync" "time" @@ -89,19 +90,35 @@ func NewAnalysis( // Get kubernetes client from viper. kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") + verbose := viper.GetBool("verbose") client, err := kubernetes.NewClient(kubecontext, kubeconfig) + if verbose { + fmt.Println("Debug: Checking kubernetes client initialization.") + } if err != nil { return nil, fmt.Errorf("initialising kubernetes client: %w", err) } + if verbose { + fmt.Printf("Debug: Kubernetes client initialized, server=%s.\n", client.Config.Host) + } // Load remote cache if it is configured. cache, err := cache.GetCacheConfiguration() + if verbose { + fmt.Println("Debug: Checking cache configuration.") + } if err != nil { return nil, err } + if verbose { + fmt.Printf("Debug: Cache configuration loaded, type=%s.\n", cache.GetName()) + } if noCache { cache.DisableCache() + if verbose { + fmt.Println("Debug: Cache disabled.") + } } a := &Analysis{ @@ -117,12 +134,31 @@ func NewAnalysis( WithDoc: withDoc, WithStats: withStats, } + if verbose { + fmt.Print("Debug: Analysis configuration loaded, ") + fmt.Printf("filters=%v, language=%s, ", filters, language) + if namespace == "" { + fmt.Printf("namespace=none, ") + } else { + fmt.Printf("namespace=%s, ", namespace) + } + if labelSelector == "" { + fmt.Printf("labelSelector=none, ") + } else { + fmt.Printf("labelSelector=%s, ", labelSelector) + } + fmt.Printf("explain=%t, maxConcurrency=%d, ", explain, maxConcurrency) + fmt.Printf("withDoc=%t, withStats=%t.\n", withDoc, withStats) + } if !explain { // Return early if AI use was not requested. return a, nil } var configAI ai.AIConfiguration + if verbose { + fmt.Println("Debug: Checking AI configuration.") + } if err := viper.UnmarshalKey("ai", &configAI); err != nil { return nil, err } @@ -135,10 +171,16 @@ func NewAnalysis( // Hence, use the default provider only if the backend is not specified by the user. if configAI.DefaultProvider != "" && backend == "" { backend = configAI.DefaultProvider + if verbose { + fmt.Printf("Debug: Using default AI provider %s.\n", backend) + } } if backend == "" { backend = "openai" + if verbose { + fmt.Printf("Debug: Using default AI provider %s.\n", backend) + } } var aiProvider ai.AIProvider @@ -153,12 +195,23 @@ func NewAnalysis( return nil, fmt.Errorf("AI provider %s not specified in configuration. Please run k8sgpt auth", backend) } + if verbose { + fmt.Printf("Debug: AI configuration loaded, provider=%s, ", backend) + fmt.Printf("baseUrl=%s, model=%s.\n", aiProvider.BaseURL, aiProvider.Model) + } + aiClient := ai.NewClient(aiProvider.Name) customHeaders := util.NewHeaders(httpHeaders) aiProvider.CustomHeaders = customHeaders + if verbose { + fmt.Println("Debug: Checking AI client initialization.") + } if err := aiClient.Configure(&aiProvider); err != nil { return nil, err } + if verbose { + fmt.Println("Debug: AI client initialized.") + } a.AIClient = aiClient a.AnalysisAIProvider = aiProvider.Name return a, nil @@ -182,6 +235,18 @@ func (a *Analysis) RunCustomAnalysis() { semaphore := make(chan struct{}, a.MaxConcurrency) var wg sync.WaitGroup var mutex sync.Mutex + verbose := viper.GetBool("verbose") + if verbose { + if len(customAnalyzers) == 0 { + fmt.Println("Debug: No custom analyzers found.") + } else { + cAnalyzerNames := make([]string, len(customAnalyzers)) + for i, cAnalyzer := range customAnalyzers { + cAnalyzerNames[i] = cAnalyzer.Name + } + fmt.Printf("Debug: Found custom analyzers %v.\n", cAnalyzerNames) + } + } for _, cAnalyzer := range customAnalyzers { wg.Add(1) semaphore <- struct{}{} @@ -194,6 +259,9 @@ func (a *Analysis) RunCustomAnalysis() { mutex.Unlock() return } + if verbose { + fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name) + } result, err := canClient.Run() if result.Kind == "" { @@ -206,10 +274,16 @@ func (a *Analysis) RunCustomAnalysis() { mutex.Lock() a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err)) mutex.Unlock() + if verbose { + fmt.Printf("Debug: %s completed with errors.\n", cAnalyzer.Name) + } } else { mutex.Lock() a.Results = append(a.Results, result) mutex.Unlock() + if verbose { + fmt.Printf("Debug: %s completed without errors.\n", cAnalyzer.Name) + } } <-semaphore }(cAnalyzer, &wg, semaphore) @@ -219,6 +293,7 @@ func (a *Analysis) RunCustomAnalysis() { func (a *Analysis) RunAnalysis() { activeFilters := viper.GetStringSlice("active_filters") + verbose := viper.GetBool("verbose") coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap() @@ -227,7 +302,13 @@ func (a *Analysis) RunAnalysis() { if a.WithDoc { var openApiErr error + if verbose { + fmt.Println("Debug: Fetching Kubernetes docs.") + } openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema() + if verbose { + fmt.Println("Debug: Checking Kubernetes docs.") + } if openApiErr != nil { a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr)) } @@ -247,6 +328,9 @@ func (a *Analysis) RunAnalysis() { var mutex sync.Mutex // if there are no filters selected and no active_filters then run coreAnalyzer if len(a.Filters) == 0 && len(activeFilters) == 0 { + if verbose { + fmt.Println("Debug: No filters selected and no active filters found, run all core analyzers.") + } for name, analyzer := range coreAnalyzerMap { wg.Add(1) semaphore <- struct{}{} @@ -258,6 +342,9 @@ func (a *Analysis) RunAnalysis() { } // if the filters flag is specified if len(a.Filters) != 0 { + if verbose { + fmt.Printf("Debug: Filter flags %v specified, run selected core analyzers.\n", a.Filters) + } for _, filter := range a.Filters { if analyzer, ok := analyzerMap[filter]; ok { semaphore <- struct{}{} @@ -272,6 +359,9 @@ func (a *Analysis) RunAnalysis() { } // use active_filters + if len(activeFilters) > 0 && verbose { + fmt.Printf("Debug: Found active filters %v, run selected core analyzers.\n", activeFilters) + } for _, filter := range activeFilters { if analyzer, ok := analyzerMap[filter]; ok { semaphore <- struct{}{} @@ -294,6 +384,10 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana } // Run the analyzer + verbose := viper.GetBool("verbose") + if verbose { + fmt.Printf("Debug: %s launched.\n", reflect.TypeOf(analyzer).Name()) + } results, err := analyzer.Analyze(analyzerConfig) if err != nil { fmt.Println(err) @@ -315,11 +409,17 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana a.Stats = append(a.Stats, stat) } a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err)) + if verbose { + fmt.Printf("Debug: %s completed with errors.\n", reflect.TypeOf(analyzer).Name()) + } } else { if a.WithStats { a.Stats = append(a.Stats, stat) } a.Results = append(a.Results, results...) + if verbose { + fmt.Printf("Debug: %s completed without errors.\n", reflect.TypeOf(analyzer).Name()) + } } <-semaphore } @@ -329,6 +429,11 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error { return nil } + verbose := viper.GetBool("verbose") + if verbose { + fmt.Println("Debug: Generating AI analysis.") + } + var bar *progressbar.ProgressBar if output != "json" { bar = progressbar.Default(int64(len(a.Results))) @@ -337,6 +442,10 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error { for index, analysis := range a.Results { var texts []string + if bar != nil && verbose { + bar.Describe(fmt.Sprintf("Analyzing %s", analysis.Kind)) + } + for _, failure := range analysis.Error { if anonymize { for _, s := range failure.Sensitive { diff --git a/pkg/analysis/analysis_test.go b/pkg/analysis/analysis_test.go index 305d1cf..ed39945 100644 --- a/pkg/analysis/analysis_test.go +++ b/pkg/analysis/analysis_test.go @@ -17,13 +17,17 @@ import ( "context" "encoding/json" "fmt" + "reflect" "strings" "testing" + "github.com/agiledragon/gomonkey/v2" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" + "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/magiconair/properties/assert" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -31,9 +35,15 @@ import ( networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" ) -// sub-function +// helper function: get type name of an analyzer +func getTypeName(i interface{}) string { + return reflect.TypeOf(i).Name() +} + +// helper function: run analysis with filter func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result { clientset := fake.NewSimpleClientset( &v1.Pod{ @@ -404,3 +414,252 @@ func TestGetAIResultForSanitizedFailures(t *testing.T) { }) } } + +// Test: Verbose output in NewAnalysis with explain=false +func TestVerbose_NewAnalysisWithoutExplain(t *testing.T) { + // Set viper config. + viper.Set("verbose", true) + viper.Set("kubecontext", "dummy") + viper.Set("kubeconfig", "dummy") + + // Patch kubernetes.NewClient to return a dummy client. + patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) { + return &kubernetes.Client{ + Config: &rest.Config{Host: "fake-server"}, + }, nil + }) + defer patches.Reset() + + output := util.CaptureOutput(func() { + a, err := NewAnalysis( + "", "english", []string{"Pod"}, "default", "", true, + false, // explain + 10, false, false, []string{}, false, + ) + require.NoError(t, err) + a.Close() + }) + + expectedOutputs := []string{ + "Debug: Checking kubernetes client initialization.", + "Debug: Kubernetes client initialized, server=fake-server.", + "Debug: Checking cache configuration.", + "Debug: Cache configuration loaded, type=file.", + "Debug: Cache disabled.", + "Debug: Analysis configuration loaded, filters=[Pod], language=english, namespace=default, labelSelector=none, explain=false, maxConcurrency=10, withDoc=false, withStats=false.", + } + for _, expected := range expectedOutputs { + if !util.Contains(output, expected) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } + } +} + +// Test: Verbose output in NewAnalysis with explain=true +func TestVerbose_NewAnalysisWithExplain(t *testing.T) { + // Set viper config. + viper.Set("verbose", true) + viper.Set("kubecontext", "dummy") + viper.Set("kubeconfig", "dummy") + // Set a dummy AI configuration. + dummyAIConfig := map[string]interface{}{ + "defaultProvider": "dummy", + "providers": []map[string]interface{}{ + { + "name": "dummy", + "baseUrl": "http://dummy", + "model": "dummy-model", + "customHeaders": map[string]string{}, + }, + }, + } + viper.Set("ai", dummyAIConfig) + + // Patch kubernetes.NewClient to return a dummy client. + patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) { + return &kubernetes.Client{ + Config: &rest.Config{Host: "fake-server"}, + }, nil + }) + defer patches.Reset() + + // Patch ai.NewClient to return a NoOp client. + patches2 := gomonkey.ApplyFunc(ai.NewClient, func(name string) ai.IAI { + return &ai.NoOpAIClient{} + }) + defer patches2.Reset() + + output := util.CaptureOutput(func() { + a, err := NewAnalysis( + "", "english", []string{"Pod"}, "default", "", true, + true, // explain + 10, false, false, []string{}, false, + ) + require.NoError(t, err) + a.Close() + }) + + expectedOutputs := []string{ + "Debug: Checking AI configuration.", + "Debug: Using default AI provider dummy.", + "Debug: AI configuration loaded, provider=dummy, baseUrl=http://dummy, model=dummy-model.", + "Debug: Checking AI client initialization.", + "Debug: AI client initialized.", + } + for _, expected := range expectedOutputs { + if !util.Contains(output, expected) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } + } +} + +// Test: Verbose output in RunAnalysis with filter flag +func TestVerbose_RunAnalysisWithFilter(t *testing.T) { + viper.Set("verbose", true) + // Run analysis with a filter flag ("Pod") to trigger debug output. + output := util.CaptureOutput(func() { + _ = analysis_RunAnalysisFilterTester(t, "Pod") + }) + + expectedOutputs := []string{ + "Debug: Filter flags [Pod] specified, run selected core analyzers.", + "Debug: PodAnalyzer launched.", + "Debug: PodAnalyzer completed without errors.", + } + + for _, expected := range expectedOutputs { + if !util.Contains(output, expected) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } + } +} + +// Test: Verbose output in RunAnalysis with active filter +func TestVerbose_RunAnalysisWithActiveFilter(t *testing.T) { + viper.Set("verbose", true) + viper.SetDefault("active_filters", "Ingress") + output := util.CaptureOutput(func() { + _ = analysis_RunAnalysisFilterTester(t, "") + }) + + expectedOutputs := []string{ + "Debug: Found active filters [Ingress], run selected core analyzers.", + "Debug: IngressAnalyzer launched.", + "Debug: IngressAnalyzer completed without errors.", + } + + for _, expected := range expectedOutputs { + if !util.Contains(output, expected) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } + } +} + +// Test: Verbose output in RunAnalysis without any filter (run all core analyzers) +func TestVerbose_RunAnalysisWithoutFilter(t *testing.T) { + viper.Set("verbose", true) + // Clear filter flag and active_filters to run all core analyzers. + viper.SetDefault("active_filters", []string{}) + output := util.CaptureOutput(func() { + _ = analysis_RunAnalysisFilterTester(t, "") + }) + + // Check for debug message indicating no filters. + expectedNoFilter := "Debug: No filters selected and no active filters found, run all core analyzers." + if !util.Contains(output, expectedNoFilter) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedNoFilter, output) + } + + // Get all core analyzers from analyzer.GetAnalyzerMap() + coreAnalyzerMap, _ := analyzer.GetAnalyzerMap() + for _, analyzerInstance := range coreAnalyzerMap { + analyzerType := getTypeName(analyzerInstance) + expectedLaunched := fmt.Sprintf("Debug: %s launched.", analyzerType) + expectedCompleted := fmt.Sprintf("Debug: %s completed without errors.", analyzerType) + if !util.Contains(output, expectedLaunched) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedLaunched, output) + } + if !util.Contains(output, expectedCompleted) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedCompleted, output) + } + } +} + +// Test: Verbose output in RunCustomAnalysis without custom analyzer +func TestVerbose_RunCustomAnalysisWithoutCustomAnalyzer(t *testing.T) { + viper.Set("verbose", true) + // Set custom_analyzers to empty array to trigger "No custom analyzers" debug message. + viper.Set("custom_analyzers", []interface{}{}) + analysisObj := &Analysis{ + MaxConcurrency: 1, + } + output := util.CaptureOutput(func() { + analysisObj.RunCustomAnalysis() + }) + expected := "Debug: No custom analyzers found." + if !util.Contains(output, "Debug: No custom analyzers found.") { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } +} + +// Test: Verbose output in RunCustomAnalysis with custom analyzer +func TestVerbose_RunCustomAnalysisWithCustomAnalyzer(t *testing.T) { + viper.Set("verbose", true) + // Set custom_analyzers with one custom analyzer using "fake" connection. + viper.Set("custom_analyzers", []map[string]interface{}{ + { + "name": "TestCustomAnalyzer", + "connection": map[string]interface{}{"url": "127.0.0.1", "port": "2333"}, + }, + }) + + analysisObj := &Analysis{ + MaxConcurrency: 1, + } + output := util.CaptureOutput(func() { + analysisObj.RunCustomAnalysis() + }) + assert.Equal(t, 1, len(analysisObj.Errors)) // connection error + + expectedOutputs := []string{ + "Debug: Found custom analyzers [TestCustomAnalyzer].", + "Debug: TestCustomAnalyzer launched.", + "Debug: TestCustomAnalyzer completed with errors.", + } + + for _, expected := range expectedOutputs { + if !util.Contains(output, expected) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } + } +} + +// Test: Verbose output in GetAIResults +func TestVerbose_GetAIResults(t *testing.T) { + viper.Set("verbose", true) + disabledCache := cache.New("disabled-cache") + disabledCache.DisableCache() + aiClient := &ai.NoOpAIClient{} + analysisObj := Analysis{ + AIClient: aiClient, + Cache: disabledCache, + Results: []common.Result{ + { + Kind: "Deployment", + Name: "test-deployment", + Error: []common.Failure{{Text: "test-problem", Sensitive: []common.Sensitive{}}}, + Details: "test-solution", + ParentObject: "parent-resource", + }, + }, + Namespace: "default", + } + output := util.CaptureOutput(func() { + _ = analysisObj.GetAIResults("json", false) + }) + + expected := "Debug: Generating AI analysis." + if !util.Contains(output, expected) { + t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go index af04fae..8030c64 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -14,6 +14,7 @@ limitations under the License. package util import ( + "bytes" "context" "crypto/rand" "crypto/sha256" @@ -311,3 +312,33 @@ func LabelStrToSelector(labelStr string) labels.Selector { } return labels.SelectorFromSet(labels.Set(labelSelectorMap)) } + +// CaptureOutput captures the output of a function that writes to stdout +func CaptureOutput(f func()) string { + old := os.Stdout + r, w, err := os.Pipe() + if err != nil { + panic(fmt.Sprintf("failed to create pipe: %v", err)) + } + os.Stdout = w + // Ensure os.Stdout is restored even if panic occurs + defer func() { + os.Stdout = old + }() + + f() + + if err := w.Close(); err != nil { + panic(fmt.Sprintf("failed to close writer: %v", err)) + } + var buf bytes.Buffer + if _, err := buf.ReadFrom(r); err != nil { + panic(fmt.Sprintf("failed to read from pipe: %v", err)) + } + return buf.String() +} + +// Contains checks if substr is present in s +func Contains(s, substr string) bool { + return bytes.Contains([]byte(s), []byte(substr)) +}