Skip to content

Commit

Permalink
enable offline mode for the analyzers
Browse files Browse the repository at this point in the history
  • Loading branch information
harshanarayana committed Jun 18, 2024
1 parent 3f80bba commit 7b3e0fe
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 2 deletions.
10 changes: 10 additions & 0 deletions cmd/analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ var (
withDoc bool
interactiveMode bool
customAnalysis bool
offlineMode bool
rcaPath string
)

// AnalyzeCmd represents the problems command
Expand All @@ -48,6 +50,10 @@ var AnalyzeCmd = &cobra.Command{
Long: `This command will find problems within your Kubernetes cluster and
provide you with a list of issues that need to be resolved`,
Run: func(cmd *cobra.Command, args []string) {
if offlineMode && rcaPath == "" {
color.Red("Offline mode of Analysis needs RCA path to be provided to extract the data")
os.Exit(1)
}
// Create analysis configuration first.
config, err := analysis.NewAnalysis(
backend,
Expand All @@ -59,6 +65,8 @@ var AnalyzeCmd = &cobra.Command{
maxConcurrency,
withDoc,
interactiveMode,
offlineMode,
rcaPath,
)

if err != nil {
Expand Down Expand Up @@ -139,4 +147,6 @@ func init() {
// custom analysis flag
AnalyzeCmd.Flags().BoolVarP(&customAnalysis, "custom-analysis", "z", false, "Enable custom analyzers")

AnalyzeCmd.Flags().BoolVar(&offlineMode, "offline-mode", false, "Run Analyzer in Offline mode from RCA collected data")
AnalyzeCmd.Flags().StringVar(&rcaPath, "rca-path", "", "Path Container RCA collected from RCA Collector infra")
}
12 changes: 11 additions & 1 deletion pkg/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes/local"
"reflect"
"strings"
"sync"
Expand Down Expand Up @@ -79,11 +80,20 @@ func NewAnalysis(
maxConcurrency int,
withDoc bool,
interactiveMode bool,
offlineMode bool,
rcaPath string,
) (*Analysis, error) {
// Get kubernetes client from viper.
kubecontext := viper.GetString("kubecontext")
kubeconfig := viper.GetString("kubeconfig")
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
var client *kubernetes.Client
var err error
if offlineMode {
client = &kubernetes.Client{}
client.Client, client.CtrlClient = local.GetLocalClient(rcaPath)
} else {
client, err = kubernetes.NewClient(kubecontext, kubeconfig)
}
if err != nil {
return nil, fmt.Errorf("initialising kubernetes client: %w", err)
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ func (c *Client) GetCtrlClient() ctrl.Client {
return c.CtrlClient
}

func (c *Client) SetKubernetesClient(k kubernetes.Interface) {
c.Client = k
}

func (c *Client) SetCtrlClient(k ctrl.Client) {
c.CtrlClient = k
}

func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
var config *rest.Config
config, err := rest.InClusterConfig()
Expand Down
153 changes: 153 additions & 0 deletions pkg/kubernetes/local/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package local

import (
"bufio"
"bytes"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
clientGoScheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/testing"
"k8s.io/klog/v2"
"os"
"path/filepath"
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
fakeCtrlclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"strings"
)

var sch *runtime.Scheme

func findFilesForResource(rcaPath string, resource string, namespace string) []string {
resource = strings.ToLower(resource)
namespace = strings.ToLower(namespace)
globPattern := fmt.Sprintf("kubectl_get_%s_*-o_yaml.log", resource)
if namespace != "" {
globPattern = fmt.Sprintf("kubectl_get_%s_--namespace_%s_*-o_yaml.log", resource, namespace)
}

files, err := filepath.Glob(rcaPath + "/" + globPattern)
if err != nil {
klog.ErrorS(err, "failed to determine the files container information for the resource", "resource", resource, "namespace", namespace, "pattern", globPattern)
return []string{}
}
return files
}

func getResourceFromFileUsingGenerics[T runtime.Object](file string, objType T) error {
data, err := os.Open(file)
if err != nil {
return err
}
defer func() {
if err := data.Close(); err != nil {
klog.ErrorS(err, "failed to close file", "file", file)
}
}()

scanner := bufio.NewScanner(data)
var buffer []byte
buf := bytes.NewBuffer(buffer)
writer := bufio.NewWriter(buf)
for scanner.Scan() {
l := scanner.Bytes()
if string(l) == "---------------------------------------------------------------------" {
break
}
if _, err := writer.WriteString(fmt.Sprintf("%s\n", string(l))); err != nil {
return err
}
}
if err := writer.Flush(); err != nil {
return err
}

decoder := yaml.NewYAMLOrJSONDecoder(bufio.NewReader(bytes.NewBuffer(buf.Bytes())), 100)
if err := decoder.Decode(objType); err != nil {
return err
}
return nil
}

func GenericFetcher[T runtime.Object](objType T, gvk schema.GroupVersionKind, resourceKind string, rcaPath string, action testing.Action) (bool, []T, error) {
files := findFilesForResource(rcaPath, resourceKind, action.GetNamespace())
list := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, 0),
}
var items []T
if len(files) > 0 {
for _, file := range files {
err := getResourceFromFileUsingGenerics(file, list)
if err != nil {
return true, nil, err
}
for _, d := range list.Items {
d.SetGroupVersionKind(gvk)
if err := sch.Convert(&d, objType, nil); err != nil {
return true, nil, err
}
if action.GetVerb() == "get" && d.GetName() == action.(testing.GetAction).GetName() && d.GetNamespace() == action.(testing.GetAction).GetNamespace() {
d.SetGroupVersionKind(gvk)
return true, []T{objType}, nil
}
items = append(items, objType)
}
}
}
if len(files) == 0 && action.GetVerb() == "get" {
return true, nil, errors.NewNotFound(action.GetResource().GroupResource(), action.(testing.GetAction).GetName())
}
return true, items, nil
}

func GetLocalClient(rcaPath string) (kubernetes.Interface, ctrl.Client) {
sch = runtime.NewScheme()
_ = scheme.AddToScheme(sch)
_ = apiextensionsv1.AddToScheme(sch)

_ = clientGoScheme.AddToScheme(sch)
fakeClient := fake.NewSimpleClientset()

fakeClient.PrependReactor("list", "deployments", func(action testing.Action) (handled bool, ret runtime.Object, err error) {
handled, items, err := GenericFetcher(&appsv1.Deployment{}, schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "deployments", rcaPath, action)
if err != nil {
return handled, nil, err
}
deployments := appsv1.DeploymentList{Items: make([]appsv1.Deployment, 0)}
for _, item := range items {
deployments.Items = append(deployments.Items, *item)
}
return handled, &deployments, nil
})

fakeClient.PrependReactor("get", "deployments", func(action testing.Action) (handled bool, ret runtime.Object, err error) {
handled, items, err := GenericFetcher(&appsv1.Deployment{}, schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "deployments", rcaPath, action)
if err != nil {
return handled, nil, err
}
return handled, items[0], nil
})

fakeClient.PrependReactor("list", "nodes", func(action testing.Action) (handled bool, ret runtime.Object, err error) {
handled, items, err := GenericFetcher(&corev1.Node{}, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}, "nodes", rcaPath, action)
if err != nil {
return handled, nil, err
}
nodes := corev1.NodeList{Items: make([]corev1.Node, 0)}
for _, item := range items {
nodes.Items = append(nodes.Items, *item)
}
return handled, &nodes, nil
})

return fakeClient, fakeCtrlclient.NewClientBuilder().WithScheme(sch).WithRuntimeObjects().Build()
}
4 changes: 3 additions & 1 deletion pkg/server/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ func (h *handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) (
i.Explain,
int(i.MaxConcurrency),
false, // Kubernetes Doc disabled in server mode
false, // Interactive mode disabled in server mode
false, // Interactive mode disabled in server mode,
false,
"",
)
config.Context = ctx // Replace context for correct timeouts.
if err != nil {
Expand Down

0 comments on commit 7b3e0fe

Please sign in to comment.