feat: enables remote custom analyzers (#906)

* feat: enables remote custom analyzers

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: fixed test that was broken

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: hiding custom analysis behind a flag

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: resolved govet issue

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: updated

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: updated

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: updated deps

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

* chore: updated deps

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
This commit is contained in:
Alex Jones 2024-02-15 07:28:00 +00:00 committed by GitHub
parent 070aa7fdd0
commit c8c9dbfadc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 127 additions and 2 deletions

View File

@ -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

View File

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

View File

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

57
pkg/custom/client.go Normal file
View File

@ -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
pkg/custom/types.go Normal file
View File

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