diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index f001a1bcb9..39ced28bd1 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -1,16 +1,12 @@ package analyze import ( - "context" "fmt" "os" "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/spf13/cobra" - "github.com/spf13/viper" ) var ( @@ -33,55 +29,13 @@ var AnalyzeCmd = &cobra.Command{ provide you with a list of issues that need to be resolved`, Run: func(cmd *cobra.Command, args []string) { - // get ai configuration - var configAI ai.AIConfiguration - err := viper.UnmarshalKey("ai", &configAI) + // AnalysisResult configuration + config, err := analysis.NewAnalysis(backend, language, filters, namespace, nocache, explain) if err != nil { color.Red("Error: %v", err) os.Exit(1) } - var aiProvider ai.AIProvider - for _, provider := range configAI.Providers { - if backend == provider.Name { - aiProvider = provider - break - } - } - - if aiProvider.Name == "" { - color.Red("Error: AI provider %s not specified in configuration. Please run k8sgpt auth", backend) - os.Exit(1) - } - - aiClient := ai.NewClient(aiProvider.Name) - if err := aiClient.Configure(aiProvider.Password, aiProvider.Model, language); err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - - ctx := context.Background() - // Get kubernetes client from viper - - kubecontext := viper.GetString("kubecontext") - kubeconfig := viper.GetString("kubeconfig") - client, err := kubernetes.NewClient(kubecontext, kubeconfig) - if err != nil { - color.Red("Error initialising kubernetes client: %v", err) - os.Exit(1) - } - - // AnalysisResult configuration - config := &analysis.Analysis{ - Namespace: namespace, - NoCache: nocache, - Filters: filters, - Explain: explain, - AIClient: aiClient, - Client: client, - Context: ctx, - } - err = config.RunAnalysis() if err != nil { color.Red("Error: %v", err) @@ -89,11 +43,6 @@ var AnalyzeCmd = &cobra.Command{ } if explain { - if len(configAI.Providers) == 0 { - color.Red("Error: AI provider not specified in configuration. Please run k8sgpt auth") - os.Exit(1) - } - err := config.GetAIResults(output, anonymize) if err != nil { color.Red("Error: %v", err) diff --git a/cmd/root.go b/cmd/root.go index 9fe5fa6ab1..b76f1f4046 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/k8sgpt-ai/k8sgpt/cmd/serve" "os" "path/filepath" @@ -53,6 +54,7 @@ func init() { rootCmd.AddCommand(filters.FiltersCmd) rootCmd.AddCommand(generate.GenerateCmd) rootCmd.AddCommand(integration.IntegrationCmd) + rootCmd.AddCommand(serve.ServeCmd) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.k8sgpt.yaml)") rootCmd.PersistentFlags().StringVar(&kubecontext, "kubecontext", "", "Kubernetes context to use. Only required if out-of-cluster.") rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", kubeconfigPath, "Path to a kubeconfig. Only required if out-of-cluster.") diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go new file mode 100644 index 0000000000..7e182e349b --- /dev/null +++ b/cmd/serve/serve.go @@ -0,0 +1,60 @@ +package serve + +import ( + "fmt" + "github.com/fatih/color" + k8sgptserver "github.com/k8sgpt-ai/k8sgpt/pkg/server" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" +) + +var ( + port string + backend string + token string +) + +var ServeCmd = &cobra.Command{ + Use: "serve", + Short: "Runs k8sgpt as a server", + Long: `Runs k8sgpt as a server to allow for easy integration with other applications.`, + Run: func(cmd *cobra.Command, args []string) { + + backendType := viper.GetString("backend_type") + if backendType == "" { + color.Red("No backend set. Please run k8sgpt auth") + os.Exit(1) + } + + if backend != "" { + backendType = backend + } + + token := viper.GetString(fmt.Sprintf("%s_key", backendType)) + // check if nil + if token == "" { + color.Red("No %s key set. Please run k8sgpt auth", backendType) + os.Exit(1) + } + + server := k8sgptserver.Config{ + Backend: backend, + Port: port, + Token: token, + } + + err := server.Serve() + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } + // override the default backend if a flag is provided + }, +} + +func init() { + // add flag for backend + ServeCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on") + ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider") +} diff --git a/go.mod b/go.mod index 53946300f6..01a354150d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/aquasecurity/trivy-operator v0.13.0 github.com/fatih/color v1.15.0 + github.com/julienschmidt/httprouter v1.3.0 github.com/magiconair/properties v1.8.7 github.com/mittwald/go-helm-client v0.12.1 github.com/sashabaranov/go-openai v1.7.0 diff --git a/go.sum b/go.sum index ddf6406be5..605018704f 100644 --- a/go.sum +++ b/go.sum @@ -409,6 +409,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 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.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= diff --git a/pkg/ai/noopai.go b/pkg/ai/noopai.go index b544109033..8ea24d8516 100644 --- a/pkg/ai/noopai.go +++ b/pkg/ai/noopai.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "github.com/fatih/color" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/spf13/viper" "strings" ) @@ -33,6 +34,7 @@ func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, nocache bool) inputKey := strings.Join(prompt, " ") // Check for cached data sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey)) + cacheKey := util.GetCacheKey(a.GetName(), sEnc) response, err := a.GetCompletion(ctx, inputKey) if err != nil { @@ -40,8 +42,8 @@ func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, nocache bool) return "", err } - if !viper.IsSet(sEnc) { - viper.Set(sEnc, base64.StdEncoding.EncodeToString([]byte(response))) + if !viper.IsSet(cacheKey) { + viper.Set(cacheKey, base64.StdEncoding.EncodeToString([]byte(response))) if err := viper.WriteConfig(); err != nil { color.Red("error writing config: %v", err) return "", nil diff --git a/pkg/ai/openai.go b/pkg/ai/openai.go index 91a70c474e..4ffca36506 100644 --- a/pkg/ai/openai.go +++ b/pkg/ai/openai.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "fmt" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" "strings" "github.com/fatih/color" @@ -58,10 +59,11 @@ func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, nocache bool) inputKey := strings.Join(prompt, " ") // Check for cached data sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey)) + cacheKey := util.GetCacheKey(a.GetName(), sEnc) // find in viper cache - if viper.IsSet(sEnc) && !nocache { + if viper.IsSet(cacheKey) && !nocache { // retrieve data from cache - response := viper.GetString(sEnc) + response := viper.GetString(cacheKey) if response == "" { color.Red("error retrieving cached data") return "", nil @@ -79,8 +81,8 @@ func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, nocache bool) return "", err } - if !viper.IsSet(sEnc) { - viper.Set(sEnc, base64.StdEncoding.EncodeToString([]byte(response))) + if !viper.IsSet(cacheKey) || nocache { + viper.Set(cacheKey, base64.StdEncoding.EncodeToString([]byte(response))) if err := viper.WriteConfig(); err != nil { color.Red("error writing config: %v", err) return "", nil diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index f2ac1a7a4e..8f3f39afb1 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "strings" "github.com/fatih/color" @@ -41,8 +42,61 @@ type JsonOutput struct { Results []common.Result `json:"results"` } -func (a *Analysis) RunAnalysis() error { +func NewAnalysis(backend string, language string, filters []string, namespace string, noCache bool, explain bool) (*Analysis, error) { + var configAI ai.AIConfiguration + err := viper.UnmarshalKey("ai", &configAI) + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } + + if len(configAI.Providers) == 0 && explain { + color.Red("Error: AI provider not specified in configuration. Please run k8sgpt auth") + os.Exit(1) + } + + var aiProvider ai.AIProvider + for _, provider := range configAI.Providers { + if backend == provider.Name { + aiProvider = provider + break + } + } + + if aiProvider.Name == "" { + color.Red("Error: AI provider %s not specified in configuration. Please run k8sgpt auth", backend) + return nil, errors.New("AI provider not specified in configuration") + } + aiClient := ai.NewClient(aiProvider.Name) + if err := aiClient.Configure(aiProvider.Password, aiProvider.Model, language); err != nil { + color.Red("Error: %v", err) + return nil, err + } + + ctx := context.Background() + // Get kubernetes client from viper + + kubecontext := viper.GetString("kubecontext") + kubeconfig := viper.GetString("kubeconfig") + client, err := kubernetes.NewClient(kubecontext, kubeconfig) + if err != nil { + color.Red("Error initialising kubernetes client: %v", err) + return nil, err + } + + return &Analysis{ + Context: ctx, + Filters: filters, + Client: client, + AIClient: aiClient, + Namespace: namespace, + NoCache: noCache, + Explain: explain, + }, nil +} + +func (a *Analysis) RunAnalysis() error { activeFilters := viper.GetStringSlice("active_filters") analyzerMap := analyzer.GetAnalyzerMap() diff --git a/pkg/server/main.go b/pkg/server/main.go new file mode 100644 index 0000000000..8dab23d571 --- /dev/null +++ b/pkg/server/main.go @@ -0,0 +1,104 @@ +package server + +import ( + json "encoding/json" + "fmt" + "github.com/fatih/color" + "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" + "net/http" + "strconv" + "strings" +) + +type Config struct { + Port string + Backend string + Key string + Token string + Output string +} + +type Health struct { + Status string `json:"status"` + Success int `json:"success"` + Failure int `json:"failure"` +} + +var health = Health{ + Status: "ok", + Success: 0, + Failure: 0, +} + +type Result struct { + Analysis []analysis.Analysis `json:"analysis"` +} + +func (s *Config) analyzeHandler(w http.ResponseWriter, r *http.Request) { + namespace := r.URL.Query().Get("namespace") + explain := getBoolParam(r.URL.Query().Get("explain")) + anonymize := getBoolParam(r.URL.Query().Get("anonymize")) + nocache := getBoolParam(r.URL.Query().Get("nocache")) + language := r.URL.Query().Get("language") + + config, err := analysis.NewAnalysis(s.Backend, language, []string{}, namespace, nocache, explain) + if err != nil { + health.Failure++ + fmt.Fprintf(w, err.Error()) + } + + err = config.RunAnalysis() + if err != nil { + color.Red("Error: %v", err) + health.Failure++ + fmt.Fprintf(w, err.Error()) + } + + if explain { + err := config.GetAIResults(s.Output, anonymize) + if err != nil { + color.Red("Error: %v", err) + health.Failure++ + fmt.Fprintf(w, err.Error()) + } + } + + output, err := config.JsonOutput() + if err != nil { + color.Red("Error: %v", err) + health.Failure++ + fmt.Fprintf(w, err.Error()) + } + health.Success++ + fmt.Fprintf(w, string(output)) +} + +func (s *Config) Serve() error { + http.HandleFunc("/analyze", s.analyzeHandler) + http.HandleFunc("/healthz", s.healthzHandler) + color.Green("Starting server on port %s", s.Port) + err := http.ListenAndServe(":"+s.Port, nil) + if err != nil { + fmt.Printf("error starting server: %s\n", err) + return err + } + return nil +} + +func (s *Config) healthzHandler(w http.ResponseWriter, r *http.Request) { + js, err := json.MarshalIndent(health, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprintf(w, string(js)) +} + +func getBoolParam(param string) bool { + b, err := strconv.ParseBool(strings.ToLower(param)) + if err != nil { + // Handle error if conversion fails + return false + } + return b +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 37f504c733..8cf273cbde 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -129,3 +129,7 @@ func ReplaceIfMatch(text string, pattern string, replacement string) string { } return text } + +func GetCacheKey(provider string, sEnc string) string { + return fmt.Sprintf("%s-%s", provider, sEnc) +}