Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enables remote custom analyzers #906

Merged
merged 11 commits into from
Feb 15, 2024
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,33 @@ k8sgpt cache remove
```
</details>

<details>
<summary> Custom Analyzers</summary>

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_

</details>

## Documentation

Expand Down
7 changes: 7 additions & 0 deletions cmd/analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
maxConcurrency int
withDoc bool
interactiveMode bool
customAnalysis bool
)

// AnalyzeCmd represents the problems command
Expand Down Expand Up @@ -65,6 +66,9 @@ var AnalyzeCmd = &cobra.Command{
}
defer config.Close()

if customAnalysis {
config.RunCustomAnalysis()
}
config.RunAnalysis()

if explain {
Expand Down Expand Up @@ -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")

}
28 changes: 26 additions & 2 deletions pkg/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -50,8 +51,10 @@ type Analysis struct {
WithDoc bool
}

type AnalysisStatus string
type AnalysisErrors []string
type (
AnalysisStatus string
AnalysisErrors []string
)

const (
StateOK AnalysisStatus = "OK"
Expand Down Expand Up @@ -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")

Expand Down
57 changes: 57 additions & 0 deletions pkg/custom/client.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions pkg/custom/types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading