1
0
mirror of https://github.com/k8sgpt-ai/k8sgpt.git synced 2025-05-06 23:26:35 +00:00

feat: add verbose flag to enable detailed output ()

* feat: add verbose flag to enable detailed output

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>

* test: add verbose output tests for analysis.go and root.go

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>

---------

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>
This commit is contained in:
Yicheng 2025-04-14 20:06:24 +08:00 committed by GitHub
parent 9ce33469d8
commit a79224e2bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 464 additions and 3 deletions

View File

@ -23,6 +23,7 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive" "github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive"
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
) )
var ( var (
@ -67,25 +68,45 @@ var AnalyzeCmd = &cobra.Command{
withStats, withStats,
) )
verbose := viper.GetBool("verbose")
if verbose {
fmt.Println("Debug: Checking analysis configuration.")
}
if err != nil { if err != nil {
color.Red("Error: %v", err) color.Red("Error: %v", err)
os.Exit(1) os.Exit(1)
} }
if verbose {
fmt.Println("Debug: Analysis initialized.")
}
defer config.Close() defer config.Close()
if customAnalysis { if customAnalysis {
config.RunCustomAnalysis() config.RunCustomAnalysis()
if verbose {
fmt.Println("Debug: All custom analyzers completed.")
}
} }
config.RunAnalysis() config.RunAnalysis()
if verbose {
fmt.Println("Debug: All core analyzers completed.")
}
if explain { 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) color.Red("Error: %v", err)
os.Exit(1) os.Exit(1)
} }
} }
// print results // print results
output_data, err := config.PrintOutput(output) output_data, err := config.PrintOutput(output)
if verbose {
fmt.Println("Debug: Checking output.")
}
if err != nil { if err != nil {
color.Red("Error: %v", err) color.Red("Error: %v", err)
os.Exit(1) os.Exit(1)

View File

@ -37,6 +37,7 @@ var (
cfgFile string cfgFile string
kubecontext string kubecontext string
kubeconfig string kubeconfig string
verbose bool
Version string Version string
Commit string Commit string
Date 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(&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(&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().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. // initConfig reads in config file and ENV variables if set.
@ -104,6 +106,7 @@ func initConfig() {
viper.Set("kubecontext", kubecontext) viper.Set("kubecontext", kubecontext)
viper.Set("kubeconfig", kubeconfig) viper.Set("kubeconfig", kubeconfig)
viper.Set("verbose", verbose)
viper.SetEnvPrefix("K8SGPT") viper.SetEnvPrefix("K8SGPT")
viper.AutomaticEnv() // read in environment variables that match viper.AutomaticEnv() // read in environment variables that match

30
cmd/root_test.go Normal file
View File

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

3
go.mod
View File

@ -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/azidentity v1.8.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
github.com/IBM/watsonx-go v1.0.1 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/aws/aws-sdk-go v1.55.6
github.com/cohere-ai/cohere-go/v2 v2.12.2 github.com/cohere-ai/cohere-go/v2 v2.12.2
github.com/go-logr/zapr v1.3.0 github.com/go-logr/zapr v1.3.0
@ -48,7 +49,6 @@ require (
github.com/pterm/pterm v0.12.80 github.com/pterm/pterm v0.12.80
google.golang.org/api v0.218.0 google.golang.org/api v0.218.0
gopkg.in/yaml.v2 v2.4.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/controller-runtime v0.19.3
sigs.k8s.io/gateway-api v1.2.1 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/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc 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/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 knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
) )

7
go.sum
View File

@ -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/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 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= 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 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/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= 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 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 h1:XhP5tVEH3ni66NSNK1+0iSO6kaGPH/6srtx6Cr+8eCg=
github.com/gophercloud/gophercloud/v2 v2.4.0/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4= 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 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 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/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.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/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/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.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= 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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 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-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-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-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-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-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=

View File

@ -18,6 +18,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -89,19 +90,35 @@ func NewAnalysis(
// Get kubernetes client from viper. // Get kubernetes client from viper.
kubecontext := viper.GetString("kubecontext") kubecontext := viper.GetString("kubecontext")
kubeconfig := viper.GetString("kubeconfig") kubeconfig := viper.GetString("kubeconfig")
verbose := viper.GetBool("verbose")
client, err := kubernetes.NewClient(kubecontext, kubeconfig) client, err := kubernetes.NewClient(kubecontext, kubeconfig)
if verbose {
fmt.Println("Debug: Checking kubernetes client initialization.")
}
if err != nil { if err != nil {
return nil, fmt.Errorf("initialising kubernetes client: %w", err) 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. // Load remote cache if it is configured.
cache, err := cache.GetCacheConfiguration() cache, err := cache.GetCacheConfiguration()
if verbose {
fmt.Println("Debug: Checking cache configuration.")
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
if verbose {
fmt.Printf("Debug: Cache configuration loaded, type=%s.\n", cache.GetName())
}
if noCache { if noCache {
cache.DisableCache() cache.DisableCache()
if verbose {
fmt.Println("Debug: Cache disabled.")
}
} }
a := &Analysis{ a := &Analysis{
@ -117,12 +134,31 @@ func NewAnalysis(
WithDoc: withDoc, WithDoc: withDoc,
WithStats: withStats, 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 { if !explain {
// Return early if AI use was not requested. // Return early if AI use was not requested.
return a, nil return a, nil
} }
var configAI ai.AIConfiguration var configAI ai.AIConfiguration
if verbose {
fmt.Println("Debug: Checking AI configuration.")
}
if err := viper.UnmarshalKey("ai", &configAI); err != nil { if err := viper.UnmarshalKey("ai", &configAI); err != nil {
return nil, err return nil, err
} }
@ -135,10 +171,16 @@ func NewAnalysis(
// Hence, use the default provider only if the backend is not specified by the user. // Hence, use the default provider only if the backend is not specified by the user.
if configAI.DefaultProvider != "" && backend == "" { if configAI.DefaultProvider != "" && backend == "" {
backend = configAI.DefaultProvider backend = configAI.DefaultProvider
if verbose {
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
}
} }
if backend == "" { if backend == "" {
backend = "openai" backend = "openai"
if verbose {
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
}
} }
var aiProvider ai.AIProvider 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) 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) aiClient := ai.NewClient(aiProvider.Name)
customHeaders := util.NewHeaders(httpHeaders) customHeaders := util.NewHeaders(httpHeaders)
aiProvider.CustomHeaders = customHeaders aiProvider.CustomHeaders = customHeaders
if verbose {
fmt.Println("Debug: Checking AI client initialization.")
}
if err := aiClient.Configure(&aiProvider); err != nil { if err := aiClient.Configure(&aiProvider); err != nil {
return nil, err return nil, err
} }
if verbose {
fmt.Println("Debug: AI client initialized.")
}
a.AIClient = aiClient a.AIClient = aiClient
a.AnalysisAIProvider = aiProvider.Name a.AnalysisAIProvider = aiProvider.Name
return a, nil return a, nil
@ -182,6 +235,18 @@ func (a *Analysis) RunCustomAnalysis() {
semaphore := make(chan struct{}, a.MaxConcurrency) semaphore := make(chan struct{}, a.MaxConcurrency)
var wg sync.WaitGroup var wg sync.WaitGroup
var mutex sync.Mutex 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 { for _, cAnalyzer := range customAnalyzers {
wg.Add(1) wg.Add(1)
semaphore <- struct{}{} semaphore <- struct{}{}
@ -194,6 +259,9 @@ func (a *Analysis) RunCustomAnalysis() {
mutex.Unlock() mutex.Unlock()
return return
} }
if verbose {
fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name)
}
result, err := canClient.Run() result, err := canClient.Run()
if result.Kind == "" { if result.Kind == "" {
@ -206,10 +274,16 @@ func (a *Analysis) RunCustomAnalysis() {
mutex.Lock() mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err)) a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err))
mutex.Unlock() mutex.Unlock()
if verbose {
fmt.Printf("Debug: %s completed with errors.\n", cAnalyzer.Name)
}
} else { } else {
mutex.Lock() mutex.Lock()
a.Results = append(a.Results, result) a.Results = append(a.Results, result)
mutex.Unlock() mutex.Unlock()
if verbose {
fmt.Printf("Debug: %s completed without errors.\n", cAnalyzer.Name)
}
} }
<-semaphore <-semaphore
}(cAnalyzer, &wg, semaphore) }(cAnalyzer, &wg, semaphore)
@ -219,6 +293,7 @@ func (a *Analysis) RunCustomAnalysis() {
func (a *Analysis) RunAnalysis() { func (a *Analysis) RunAnalysis() {
activeFilters := viper.GetStringSlice("active_filters") activeFilters := viper.GetStringSlice("active_filters")
verbose := viper.GetBool("verbose")
coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap() coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap()
@ -227,7 +302,13 @@ func (a *Analysis) RunAnalysis() {
if a.WithDoc { if a.WithDoc {
var openApiErr error var openApiErr error
if verbose {
fmt.Println("Debug: Fetching Kubernetes docs.")
}
openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema() openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema()
if verbose {
fmt.Println("Debug: Checking Kubernetes docs.")
}
if openApiErr != nil { if openApiErr != nil {
a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr)) a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr))
} }
@ -247,6 +328,9 @@ func (a *Analysis) RunAnalysis() {
var mutex sync.Mutex var mutex sync.Mutex
// if there are no filters selected and no active_filters then run coreAnalyzer // if there are no filters selected and no active_filters then run coreAnalyzer
if len(a.Filters) == 0 && len(activeFilters) == 0 { 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 { for name, analyzer := range coreAnalyzerMap {
wg.Add(1) wg.Add(1)
semaphore <- struct{}{} semaphore <- struct{}{}
@ -258,6 +342,9 @@ func (a *Analysis) RunAnalysis() {
} }
// if the filters flag is specified // if the filters flag is specified
if len(a.Filters) != 0 { 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 { for _, filter := range a.Filters {
if analyzer, ok := analyzerMap[filter]; ok { if analyzer, ok := analyzerMap[filter]; ok {
semaphore <- struct{}{} semaphore <- struct{}{}
@ -272,6 +359,9 @@ func (a *Analysis) RunAnalysis() {
} }
// use active_filters // 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 { for _, filter := range activeFilters {
if analyzer, ok := analyzerMap[filter]; ok { if analyzer, ok := analyzerMap[filter]; ok {
semaphore <- struct{}{} semaphore <- struct{}{}
@ -294,6 +384,10 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
} }
// Run the analyzer // Run the analyzer
verbose := viper.GetBool("verbose")
if verbose {
fmt.Printf("Debug: %s launched.\n", reflect.TypeOf(analyzer).Name())
}
results, err := analyzer.Analyze(analyzerConfig) results, err := analyzer.Analyze(analyzerConfig)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -315,11 +409,17 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
a.Stats = append(a.Stats, stat) a.Stats = append(a.Stats, stat)
} }
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err)) 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 { } else {
if a.WithStats { if a.WithStats {
a.Stats = append(a.Stats, stat) a.Stats = append(a.Stats, stat)
} }
a.Results = append(a.Results, results...) a.Results = append(a.Results, results...)
if verbose {
fmt.Printf("Debug: %s completed without errors.\n", reflect.TypeOf(analyzer).Name())
}
} }
<-semaphore <-semaphore
} }
@ -329,6 +429,11 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
return nil return nil
} }
verbose := viper.GetBool("verbose")
if verbose {
fmt.Println("Debug: Generating AI analysis.")
}
var bar *progressbar.ProgressBar var bar *progressbar.ProgressBar
if output != "json" { if output != "json" {
bar = progressbar.Default(int64(len(a.Results))) 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 { for index, analysis := range a.Results {
var texts []string var texts []string
if bar != nil && verbose {
bar.Describe(fmt.Sprintf("Analyzing %s", analysis.Kind))
}
for _, failure := range analysis.Error { for _, failure := range analysis.Error {
if anonymize { if anonymize {
for _, s := range failure.Sensitive { for _, s := range failure.Sensitive {

View File

@ -17,13 +17,17 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"strings" "strings"
"testing" "testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai" "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/cache"
"github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/magiconair/properties/assert" "github.com/magiconair/properties/assert"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -31,9 +35,15 @@ import (
networkingv1 "k8s.io/api/networking/v1" networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake" "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 { func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result {
clientset := fake.NewSimpleClientset( clientset := fake.NewSimpleClientset(
&v1.Pod{ &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)
}
}

View File

@ -14,6 +14,7 @@ limitations under the License.
package util package util
import ( import (
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
@ -311,3 +312,33 @@ func LabelStrToSelector(labelStr string) labels.Selector {
} }
return labels.SelectorFromSet(labels.Set(labelSelectorMap)) 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))
}