diff --git a/README.md b/README.md index 24580041b2..4c5022cf61 100644 --- a/README.md +++ b/README.md @@ -459,6 +459,33 @@ k8sgpt cache remove ``` +
+ Custom Analyzers + +There may be scenarios where you wish to write your own analyzer in a language of your choice. +K8sGPT now supports the ability to do so by abiding by the [schema](https://github.com/k8sgpt-ai/schemas/blob/main/protobuf/schema/v1/analyzer.proto) and serving the analyzer for consumption. +To do so, define the analyzer within the K8sGPT configuration and it will add it into the scanning process. +In addition to this you will need to enable the following flag on analysis: +``` +k8sgpt analyze --custom-analysis +``` + +Here is an example local host analyzer in [Rust](https://github.com/k8sgpt-ai/host-analyzer) +When this is run on `localhost:8080` the K8sGPT config can pick it up with the following additions: + +``` +custom_analyzers: + - name: host-analyzer + connection: + url: localhost + port: 8080 +``` + +This now gives the ability to pass through hostOS information ( from this analyzer example ) to K8sGPT to use as context with normal analysis. + +_See the docs on how to write a custom analyzer_ + +
## Documentation diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index d9b85825f0..4c7480c1da 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -37,6 +37,7 @@ var ( maxConcurrency int withDoc bool interactiveMode bool + customAnalysis bool ) // AnalyzeCmd represents the problems command @@ -65,6 +66,9 @@ var AnalyzeCmd = &cobra.Command{ } defer config.Close() + if customAnalysis { + config.RunCustomAnalysis() + } config.RunAnalysis() if explain { @@ -131,4 +135,7 @@ func init() { AnalyzeCmd.Flags().BoolVarP(&withDoc, "with-doc", "d", false, "Give me the official documentation of the involved field") // interactive mode flag AnalyzeCmd.Flags().BoolVarP(&interactiveMode, "interactive", "i", false, "Enable interactive mode that allows further conversation with LLM about the problem. Works only with --explain flag") + // custom analysis flag + AnalyzeCmd.Flags().BoolVarP(&customAnalysis, "custom-analysis", "z", false, "Enable custom analyzers") + } diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index fddd302931..139d0e7505 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -28,6 +28,7 @@ import ( "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/custom" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/schollz/progressbar/v3" @@ -50,8 +51,10 @@ type Analysis struct { WithDoc bool } -type AnalysisStatus string -type AnalysisErrors []string +type ( + AnalysisStatus string + AnalysisErrors []string +) const ( StateOK AnalysisStatus = "OK" @@ -147,6 +150,27 @@ func NewAnalysis( return a, nil } +func (a *Analysis) RunCustomAnalysis() { + var customAnalyzers []custom.CustomAnalyzer + if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil { + a.Errors = append(a.Errors, err.Error()) + } + + for _, cAnalyzer := range customAnalyzers { + + canClient, err := custom.NewClient(cAnalyzer.Connection) + if err != nil { + a.Errors = append(a.Errors, fmt.Sprintf("Client creation error for %s analyzer", cAnalyzer.Name)) + continue + } + + result, err := canClient.Run() + if err != nil { + a.Results = append(a.Results, result) + } + } +} + func (a *Analysis) RunAnalysis() { activeFilters := viper.GetStringSlice("active_filters") diff --git a/pkg/custom/client.go b/pkg/custom/client.go new file mode 100644 index 0000000000..e0d3977b93 --- /dev/null +++ b/pkg/custom/client.go @@ -0,0 +1,57 @@ +package custom + +import ( + rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc" + schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" + "context" + "fmt" + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type Client struct { + c *grpc.ClientConn + analyzerClient rpc.AnalyzerServiceClient +} + +func NewClient(c Connection) (*Client, error) { + + conn, err := grpc.Dial(fmt.Sprintf("%s:%s", c.Url, c.Port), grpc.WithTransportCredentials(insecure.NewCredentials())) + + if err != nil { + return nil, err + } + client := rpc.NewAnalyzerServiceClient(conn) + return &Client{ + c: conn, + analyzerClient: client, + }, nil +} + +func (cli *Client) Run() (common.Result, error) { + var result common.Result + req := &schemav1.AnalyzerRunRequest{} + res, err := cli.analyzerClient.Run(context.Background(), req) + if err != nil { + return result, err + } + if res.Result != nil { + + // We should refactor this, because Error and Failure do not map 1:1 from K8sGPT/schema + var errorsFound []common.Failure + for _, e := range res.Result.Error { + errorsFound = append(errorsFound, common.Failure{ + Text: e.Text, + // TODO: Support sensitive data + }) + } + + result.Name = res.Result.Name + result.Kind = res.Result.Kind + result.Details = res.Result.Details + result.ParentObject = res.Result.ParentObject + result.Error = errorsFound + } + return result, nil +} diff --git a/pkg/custom/types.go b/pkg/custom/types.go new file mode 100644 index 0000000000..413dbdbe23 --- /dev/null +++ b/pkg/custom/types.go @@ -0,0 +1,10 @@ +package custom + +type Connection struct { + Url string `json:"url"` + Port string `json:"port"` +} +type CustomAnalyzer struct { + Name string `json:"name"` + Connection Connection `json:"connection"` +}